@fredrika/mcp-mochi 1.0.5 → 1.0.6-beta.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/README.md CHANGED
@@ -5,126 +5,149 @@ This MCP server provides integration with the Mochi flashcard system, allowing y
5
5
  ## Features
6
6
 
7
7
  - Create, update, and delete flashcards
8
+ - Create cards from templates with automatic field name-to-ID mapping
9
+ - Add attachments (images, audio) to cards
10
+ - Get cards due for review
8
11
  - List flashcards, decks, and templates
9
12
 
10
- ## Setup
13
+ ## Usage with Claude Desktop
14
+
15
+ Add the following to your `claude_desktop_config.json`:
16
+
17
+ ### NPX (recommended)
18
+
19
+ ```json
20
+ {
21
+ "mcpServers": {
22
+ "mochi": {
23
+ "command": "npx",
24
+ "args": ["-y", "@fredrika/mcp-mochi"],
25
+ "env": {
26
+ "MOCHI_API_KEY": "<YOUR_TOKEN>"
27
+ }
28
+ }
29
+ }
30
+ }
31
+ ```
32
+
33
+ ### Local Development
34
+
35
+ ```json
36
+ {
37
+ "mcpServers": {
38
+ "mochi": {
39
+ "command": "node",
40
+ "args": ["/path/to/mcp-mochi/dist/index.js"],
41
+ "env": {
42
+ "MOCHI_API_KEY": "<YOUR_TOKEN>"
43
+ }
44
+ }
45
+ }
46
+ }
47
+ ```
48
+
49
+ ## Local Development Setup
11
50
 
12
- 1. Install dependencies:
51
+ 1. Clone and install dependencies:
13
52
  ```bash
53
+ git clone https://github.com/fredrika/mcp-mochi.git
54
+ cd mcp-mochi
14
55
  npm install
15
56
  ```
16
57
 
17
- 2. Configure your Mochi API token:
18
- - Copy `.env.example` to `.env`
19
- - Replace `your_mochi_api_token_here` with your actual Mochi API token
20
-
21
- 3. Build the project:
58
+ 2. Build the project:
22
59
  ```bash
23
60
  npm run build
24
61
  ```
25
62
 
26
- 4. Start the server:
63
+ 3. Test with MCP Inspector:
27
64
  ```bash
28
- npm start
65
+ MOCHI_API_KEY=<YOUR_TOKEN> npx @modelcontextprotocol/inspector node dist/index.js
29
66
  ```
30
67
 
31
68
  ## Available Tools
32
69
 
33
- ### `mochi_create_card`
70
+ ### `mochi_create_flashcard`
34
71
  Create a new flashcard in Mochi.
35
- - Parameters:
36
- - `content`: string (Markdown content of the card. Separate front and back using a horizontal rule `---`)
37
- - `deck-id`: string (ID of the deck to create the card in)
38
- - `template-id`: string (optional, template to use for the card)
39
- - `manual-tags`: string[] (optional, tags to add to the card)
40
- - `fields`: object (map of field IDs to field values, required if using a template)
41
-
42
- ### `mochi_update_card`
43
- Update or delete an existing flashcard in Mochi. To delete, set `trashed` to true.
44
- - Parameters:
45
- - `card-id`: string (ID of the card to update)
46
- - Any updatable card fields (see code for all options)
47
- - To delete: set `trashed?` to `'true'` (string)
48
-
49
- ### `mochi_list_cards`
50
- List cards (paginated).
51
- - Parameters:
52
- - `deck-id`: string (optional, filter by deck)
53
- - `limit`: number (optional, 1-100)
54
- - `bookmark`: string (optional, for pagination)
72
+ - `content`: Markdown content (separate front/back with `---`)
73
+ - `deck-id`: ID of the deck (use `mochi_list_decks` to find)
74
+ - `template-id`: (optional) Template to use
75
+ - `fields`: (optional) Map of field IDs to values (required with template)
76
+ - `manual-tags`: (optional) Array of tags
77
+
78
+ ### `mochi_create_card_from_template`
79
+ Create a flashcard using a template with field **names** (not IDs). The MCP automatically maps names to IDs.
80
+ - `template-id`: Template ID (use `mochi_list_templates` to find)
81
+ - `deck-id`: Deck ID
82
+ - `fields`: Map of field names to values (e.g., `{"Front": "Question?", "Back": "Answer"}`)
83
+ - `manual-tags`: (optional) Array of tags
84
+
85
+ ### `mochi_update_flashcard`
86
+ Update or delete a flashcard. Set `trashed?` to `true` to delete.
87
+ - `card-id`: ID of the card to update
88
+ - Any updatable card fields
89
+
90
+ ### `mochi_add_attachment`
91
+ Add an attachment (image, audio, etc.) to a card using base64 data.
92
+ - `card-id`: ID of the card
93
+ - `data`: Base64-encoded file data
94
+ - `filename`: Filename with extension (e.g., `image.png`)
95
+ - `content-type`: (optional) MIME type (inferred from filename if omitted)
96
+
97
+ ### `mochi_list_flashcards`
98
+ List flashcards (paginated).
99
+ - `deck-id`: (optional) Filter by deck
100
+ - `limit`: (optional) 1-100
101
+ - `bookmark`: (optional) Pagination token
55
102
 
56
103
  ### `mochi_list_decks`
57
104
  List all decks.
58
- - Parameters:
59
- - `bookmark`: string (optional, for pagination)
105
+ - `bookmark`: (optional) Pagination token
60
106
 
61
107
  ### `mochi_list_templates`
62
- List all templates.
63
- - Parameters:
64
- - `bookmark`: string (optional, for pagination)
65
-
66
- ## Example Usage
108
+ List all templates with their field definitions.
109
+ - `bookmark`: (optional) Pagination token
67
110
 
68
- Here's how to use the MCP server with the MCP Inspector:
69
-
70
- 1. Start the server:
71
- ```bash
72
- npm start
73
- ```
111
+ ### `mochi_get_due_cards`
112
+ Get flashcards due for review.
113
+ - `deck-id`: (optional) Filter by deck
114
+ - `date`: (optional) ISO 8601 date (defaults to today)
74
115
 
75
- 2. In another terminal, use the MCP Inspector to interact with the server:
76
- ```bash
77
- mcp-inspector
78
- ```
116
+ ## Examples
79
117
 
80
- 3. Create a new flashcard:
81
- ```json
82
- {
83
- "tool": "mochi_create_card",
84
- "params": {
85
- "content": "What is MCP?\n---\nModel Context Protocol - a protocol for providing context to LLMs",
86
- "deck-id": "<YOUR_DECK_ID>"
87
- }
88
- }
89
- ```
118
+ ### Create a simple flashcard
90
119
 
91
- 4. List all decks:
92
- ```json
93
- {
94
- "tool": "mochi_list_decks",
95
- "params": {}
96
- }
97
- ```
98
-
99
- 5. Delete a flashcard (set `trashed` to true via update):
100
- ```json
101
- {
102
- "tool": "mochi_update_card",
103
- "params": {
104
- "card-id": "<CARD_ID>",
105
- "trashed?": "true"
106
- }
107
- }
108
- ```
109
-
110
- ## Usage with Claude Desktop
111
- To use this with Claude Desktop, add the following to your `claude_desktop_config.json`:
120
+ ```json
121
+ {
122
+ "tool": "mochi_create_flashcard",
123
+ "params": {
124
+ "content": "What is MCP?\n---\nModel Context Protocol - a protocol for providing context to LLMs",
125
+ "deck-id": "<DECK_ID>"
126
+ }
127
+ }
128
+ ```
112
129
 
113
- ### NPX
130
+ ### Create a card from template
114
131
 
115
132
  ```json
116
133
  {
117
- "mcpServers": {
118
- "mochi": {
119
- "command": "npx",
120
- "args": [
121
- "-y",
122
- "@fredrika/mcp-mochi"
123
- ],
124
- "env": {
125
- "MOCHI_API_KEY": "<YOUR_TOKEN>"
126
- }
134
+ "tool": "mochi_create_card_from_template",
135
+ "params": {
136
+ "template-id": "<TEMPLATE_ID>",
137
+ "deck-id": "<DECK_ID>",
138
+ "fields": {
139
+ "Front": "What is the capital of France?",
140
+ "Back": "Paris"
127
141
  }
128
142
  }
129
143
  }
130
144
  ```
145
+
146
+ ### Get today's due cards
147
+
148
+ ```json
149
+ {
150
+ "tool": "mochi_get_due_cards",
151
+ "params": {}
152
+ }
153
+ ```
package/dist/index.d.ts CHANGED
@@ -98,6 +98,117 @@ declare const ListTemplatesParamsSchema: z.ZodObject<{
98
98
  }, {
99
99
  bookmark?: string | undefined;
100
100
  }>;
101
+ declare const GetDueCardsParamsSchema: z.ZodObject<{
102
+ "deck-id": z.ZodOptional<z.ZodString>;
103
+ date: z.ZodOptional<z.ZodString>;
104
+ }, "strip", z.ZodTypeAny, {
105
+ date?: string | undefined;
106
+ "deck-id"?: string | undefined;
107
+ }, {
108
+ date?: string | undefined;
109
+ "deck-id"?: string | undefined;
110
+ }>;
111
+ declare const CreateCardFromTemplateSchema: z.ZodObject<{
112
+ "template-id": z.ZodString;
113
+ "deck-id": z.ZodString;
114
+ fields: z.ZodRecord<z.ZodString, z.ZodString>;
115
+ "manual-tags": z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
116
+ }, "strip", z.ZodTypeAny, {
117
+ "deck-id": string;
118
+ "template-id": string;
119
+ fields: Record<string, string>;
120
+ "manual-tags"?: string[] | undefined;
121
+ }, {
122
+ "deck-id": string;
123
+ "template-id": string;
124
+ fields: Record<string, string>;
125
+ "manual-tags"?: string[] | undefined;
126
+ }>;
127
+ declare const AddAttachmentSchema: z.ZodObject<{
128
+ "card-id": z.ZodString;
129
+ data: z.ZodString;
130
+ filename: z.ZodString;
131
+ "content-type": z.ZodOptional<z.ZodString>;
132
+ }, "strip", z.ZodTypeAny, {
133
+ data: string;
134
+ filename: string;
135
+ "card-id": string;
136
+ "content-type"?: string | undefined;
137
+ }, {
138
+ data: string;
139
+ filename: string;
140
+ "card-id": string;
141
+ "content-type"?: string | undefined;
142
+ }>;
143
+ type AddAttachmentRequest = z.infer<typeof AddAttachmentSchema>;
144
+ declare const TemplateSchema: z.ZodObject<{
145
+ id: z.ZodString;
146
+ name: z.ZodString;
147
+ content: z.ZodString;
148
+ pos: z.ZodString;
149
+ fields: z.ZodRecord<z.ZodString, z.ZodObject<{
150
+ id: z.ZodString;
151
+ name: z.ZodString;
152
+ pos: z.ZodString;
153
+ type: z.ZodNullable<z.ZodOptional<z.ZodString>>;
154
+ source: z.ZodNullable<z.ZodOptional<z.ZodString>>;
155
+ options: z.ZodOptional<z.ZodObject<{
156
+ "multi-line?": z.ZodOptional<z.ZodBoolean>;
157
+ }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
158
+ "multi-line?": z.ZodOptional<z.ZodBoolean>;
159
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
160
+ "multi-line?": z.ZodOptional<z.ZodBoolean>;
161
+ }, z.ZodTypeAny, "passthrough">>>;
162
+ }, "strip", z.ZodTypeAny, {
163
+ name: string;
164
+ id: string;
165
+ pos: string;
166
+ type?: string | null | undefined;
167
+ source?: string | null | undefined;
168
+ options?: z.objectOutputType<{
169
+ "multi-line?": z.ZodOptional<z.ZodBoolean>;
170
+ }, z.ZodTypeAny, "passthrough"> | undefined;
171
+ }, {
172
+ name: string;
173
+ id: string;
174
+ pos: string;
175
+ type?: string | null | undefined;
176
+ source?: string | null | undefined;
177
+ options?: z.objectInputType<{
178
+ "multi-line?": z.ZodOptional<z.ZodBoolean>;
179
+ }, z.ZodTypeAny, "passthrough"> | undefined;
180
+ }>>;
181
+ }, "strip", z.ZodTypeAny, {
182
+ name: string;
183
+ id: string;
184
+ content: string;
185
+ fields: Record<string, {
186
+ name: string;
187
+ id: string;
188
+ pos: string;
189
+ type?: string | null | undefined;
190
+ source?: string | null | undefined;
191
+ options?: z.objectOutputType<{
192
+ "multi-line?": z.ZodOptional<z.ZodBoolean>;
193
+ }, z.ZodTypeAny, "passthrough"> | undefined;
194
+ }>;
195
+ pos: string;
196
+ }, {
197
+ name: string;
198
+ id: string;
199
+ content: string;
200
+ fields: Record<string, {
201
+ name: string;
202
+ id: string;
203
+ pos: string;
204
+ type?: string | null | undefined;
205
+ source?: string | null | undefined;
206
+ options?: z.objectInputType<{
207
+ "multi-line?": z.ZodOptional<z.ZodBoolean>;
208
+ }, z.ZodTypeAny, "passthrough"> | undefined;
209
+ }>;
210
+ pos: string;
211
+ }>;
101
212
  declare const ListTemplatesResponseSchema: z.ZodObject<{
102
213
  bookmark: z.ZodString;
103
214
  docs: z.ZodArray<z.ZodObject<{
@@ -109,27 +220,33 @@ declare const ListTemplatesResponseSchema: z.ZodObject<{
109
220
  id: z.ZodString;
110
221
  name: z.ZodString;
111
222
  pos: z.ZodString;
223
+ type: z.ZodNullable<z.ZodOptional<z.ZodString>>;
224
+ source: z.ZodNullable<z.ZodOptional<z.ZodString>>;
112
225
  options: z.ZodOptional<z.ZodObject<{
113
226
  "multi-line?": z.ZodOptional<z.ZodBoolean>;
114
- }, "strip", z.ZodTypeAny, {
115
- "multi-line?"?: boolean | undefined;
116
- }, {
117
- "multi-line?"?: boolean | undefined;
118
- }>>;
227
+ }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
228
+ "multi-line?": z.ZodOptional<z.ZodBoolean>;
229
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
230
+ "multi-line?": z.ZodOptional<z.ZodBoolean>;
231
+ }, z.ZodTypeAny, "passthrough">>>;
119
232
  }, "strip", z.ZodTypeAny, {
120
233
  name: string;
121
234
  id: string;
122
235
  pos: string;
123
- options?: {
124
- "multi-line?"?: boolean | undefined;
125
- } | undefined;
236
+ type?: string | null | undefined;
237
+ source?: string | null | undefined;
238
+ options?: z.objectOutputType<{
239
+ "multi-line?": z.ZodOptional<z.ZodBoolean>;
240
+ }, z.ZodTypeAny, "passthrough"> | undefined;
126
241
  }, {
127
242
  name: string;
128
243
  id: string;
129
244
  pos: string;
130
- options?: {
131
- "multi-line?"?: boolean | undefined;
132
- } | undefined;
245
+ type?: string | null | undefined;
246
+ source?: string | null | undefined;
247
+ options?: z.objectInputType<{
248
+ "multi-line?": z.ZodOptional<z.ZodBoolean>;
249
+ }, z.ZodTypeAny, "passthrough"> | undefined;
133
250
  }>>;
134
251
  }, "strip", z.ZodTypeAny, {
135
252
  name: string;
@@ -139,9 +256,11 @@ declare const ListTemplatesResponseSchema: z.ZodObject<{
139
256
  name: string;
140
257
  id: string;
141
258
  pos: string;
142
- options?: {
143
- "multi-line?"?: boolean | undefined;
144
- } | undefined;
259
+ type?: string | null | undefined;
260
+ source?: string | null | undefined;
261
+ options?: z.objectOutputType<{
262
+ "multi-line?": z.ZodOptional<z.ZodBoolean>;
263
+ }, z.ZodTypeAny, "passthrough"> | undefined;
145
264
  }>;
146
265
  pos: string;
147
266
  }, {
@@ -152,9 +271,11 @@ declare const ListTemplatesResponseSchema: z.ZodObject<{
152
271
  name: string;
153
272
  id: string;
154
273
  pos: string;
155
- options?: {
156
- "multi-line?"?: boolean | undefined;
157
- } | undefined;
274
+ type?: string | null | undefined;
275
+ source?: string | null | undefined;
276
+ options?: z.objectInputType<{
277
+ "multi-line?": z.ZodOptional<z.ZodBoolean>;
278
+ }, z.ZodTypeAny, "passthrough"> | undefined;
158
279
  }>;
159
280
  pos: string;
160
281
  }>, "many">;
@@ -168,9 +289,11 @@ declare const ListTemplatesResponseSchema: z.ZodObject<{
168
289
  name: string;
169
290
  id: string;
170
291
  pos: string;
171
- options?: {
172
- "multi-line?"?: boolean | undefined;
173
- } | undefined;
292
+ type?: string | null | undefined;
293
+ source?: string | null | undefined;
294
+ options?: z.objectOutputType<{
295
+ "multi-line?": z.ZodOptional<z.ZodBoolean>;
296
+ }, z.ZodTypeAny, "passthrough"> | undefined;
174
297
  }>;
175
298
  pos: string;
176
299
  }[];
@@ -184,9 +307,11 @@ declare const ListTemplatesResponseSchema: z.ZodObject<{
184
307
  name: string;
185
308
  id: string;
186
309
  pos: string;
187
- options?: {
188
- "multi-line?"?: boolean | undefined;
189
- } | undefined;
310
+ type?: string | null | undefined;
311
+ source?: string | null | undefined;
312
+ options?: z.objectInputType<{
313
+ "multi-line?": z.ZodOptional<z.ZodBoolean>;
314
+ }, z.ZodTypeAny, "passthrough"> | undefined;
190
315
  }>;
191
316
  pos: string;
192
317
  }[];
@@ -197,6 +322,8 @@ type ListCardsParams = z.infer<typeof ListCardsParamsSchema>;
197
322
  type ListDecksParams = z.infer<typeof ListDecksParamsSchema>;
198
323
  type CreateCardRequest = z.infer<typeof CreateCardRequestSchema>;
199
324
  type UpdateCardRequest = z.infer<typeof UpdateCardRequestSchema>;
325
+ type GetDueCardsParams = z.infer<typeof GetDueCardsParamsSchema>;
326
+ type CreateCardFromTemplateParams = z.infer<typeof CreateCardFromTemplateSchema>;
200
327
  declare const CreateCardResponseSchema: z.ZodObject<{
201
328
  id: z.ZodString;
202
329
  tags: z.ZodArray<z.ZodString, "many">;
@@ -273,17 +400,30 @@ declare const ListDecksResponseSchema: z.ZodObject<{
273
400
  id: z.ZodString;
274
401
  sort: z.ZodNumber;
275
402
  name: z.ZodString;
276
- archived: z.ZodOptional<z.ZodBoolean>;
403
+ "archived?": z.ZodNullable<z.ZodOptional<z.ZodBoolean>>;
404
+ "trashed?": z.ZodNullable<z.ZodOptional<z.ZodObject<{
405
+ date: z.ZodString;
406
+ }, "strip", z.ZodTypeAny, {
407
+ date: string;
408
+ }, {
409
+ date: string;
410
+ }>>>;
277
411
  }, "strip", z.ZodTypeAny, {
278
412
  sort: number;
279
413
  name: string;
280
414
  id: string;
281
- archived?: boolean | undefined;
415
+ "archived?"?: boolean | null | undefined;
416
+ "trashed?"?: {
417
+ date: string;
418
+ } | null | undefined;
282
419
  }, {
283
420
  sort: number;
284
421
  name: string;
285
422
  id: string;
286
- archived?: boolean | undefined;
423
+ "archived?"?: boolean | null | undefined;
424
+ "trashed?"?: {
425
+ date: string;
426
+ } | null | undefined;
287
427
  }>, "many">;
288
428
  }, "strip", z.ZodTypeAny, {
289
429
  bookmark: string;
@@ -291,7 +431,10 @@ declare const ListDecksResponseSchema: z.ZodObject<{
291
431
  sort: number;
292
432
  name: string;
293
433
  id: string;
294
- archived?: boolean | undefined;
434
+ "archived?"?: boolean | null | undefined;
435
+ "trashed?"?: {
436
+ date: string;
437
+ } | null | undefined;
295
438
  }[];
296
439
  }, {
297
440
  bookmark: string;
@@ -299,9 +442,49 @@ declare const ListDecksResponseSchema: z.ZodObject<{
299
442
  sort: number;
300
443
  name: string;
301
444
  id: string;
302
- archived?: boolean | undefined;
445
+ "archived?"?: boolean | null | undefined;
446
+ "trashed?"?: {
447
+ date: string;
448
+ } | null | undefined;
303
449
  }[];
304
450
  }>;
451
+ declare const GetDueCardsResponseSchema: z.ZodObject<{
452
+ cards: z.ZodArray<z.ZodObject<{
453
+ id: z.ZodString;
454
+ content: z.ZodString;
455
+ name: z.ZodString;
456
+ "deck-id": z.ZodString;
457
+ "new?": z.ZodBoolean;
458
+ }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
459
+ id: z.ZodString;
460
+ content: z.ZodString;
461
+ name: z.ZodString;
462
+ "deck-id": z.ZodString;
463
+ "new?": z.ZodBoolean;
464
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
465
+ id: z.ZodString;
466
+ content: z.ZodString;
467
+ name: z.ZodString;
468
+ "deck-id": z.ZodString;
469
+ "new?": z.ZodBoolean;
470
+ }, z.ZodTypeAny, "passthrough">>, "many">;
471
+ }, "strip", z.ZodTypeAny, {
472
+ cards: z.objectOutputType<{
473
+ id: z.ZodString;
474
+ content: z.ZodString;
475
+ name: z.ZodString;
476
+ "deck-id": z.ZodString;
477
+ "new?": z.ZodBoolean;
478
+ }, z.ZodTypeAny, "passthrough">[];
479
+ }, {
480
+ cards: z.objectInputType<{
481
+ id: z.ZodString;
482
+ content: z.ZodString;
483
+ name: z.ZodString;
484
+ "deck-id": z.ZodString;
485
+ "new?": z.ZodBoolean;
486
+ }, z.ZodTypeAny, "passthrough">[];
487
+ }>;
305
488
  export declare class MochiClient {
306
489
  private api;
307
490
  private token;
@@ -311,5 +494,12 @@ export declare class MochiClient {
311
494
  listDecks(params?: ListDecksParams): Promise<ListDecksResponse>;
312
495
  listCards(params?: ListCardsParams): Promise<ListCardsResponse>;
313
496
  listTemplates(params?: ListTemplatesParams): Promise<ListTemplatesResponse>;
497
+ getDueCards(params?: GetDueCardsParams): Promise<z.infer<typeof GetDueCardsResponseSchema>>;
498
+ getTemplate(templateId: string): Promise<z.infer<typeof TemplateSchema>>;
499
+ createCardFromTemplate(request: CreateCardFromTemplateParams): Promise<CreateCardResponse>;
500
+ addAttachment(request: AddAttachmentRequest): Promise<{
501
+ filename: string;
502
+ markdown: string;
503
+ }>;
314
504
  }
315
505
  export {};
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
3
3
  import axios from "axios";
4
+ import FormData from "form-data";
4
5
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
5
6
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
7
  import dotenv from "dotenv";
@@ -97,10 +98,60 @@ const ListTemplatesParamsSchema = z.object({
97
98
  .optional()
98
99
  .describe("Pagination bookmark for fetching next page of results"),
99
100
  });
101
+ const GetDueCardsParamsSchema = z.object({
102
+ "deck-id": z
103
+ .string()
104
+ .optional()
105
+ .describe("Optional deck ID to filter due cards by a specific deck"),
106
+ date: z
107
+ .string()
108
+ .optional()
109
+ .describe("Optional ISO 8601 date to get cards due on that date. Defaults to today."),
110
+ });
111
+ const CreateCardFromTemplateSchema = z.object({
112
+ "template-id": z
113
+ .string()
114
+ .min(1)
115
+ .describe("ID of the template to use. Get this from mochi_list_templates."),
116
+ "deck-id": z
117
+ .string()
118
+ .min(1)
119
+ .describe("ID of the deck to create the card in. Get this from mochi_list_decks."),
120
+ fields: z
121
+ .record(z.string(), z.string())
122
+ .describe('Map of field NAMES (not IDs) to values. E.g., { "Word": "serendipity" } or { "Front": "Question?", "Back": "Answer" }'),
123
+ "manual-tags": z
124
+ .array(z.string())
125
+ .optional()
126
+ .describe("Optional array of tags to add to the card"),
127
+ });
128
+ // Schema for adding attachments
129
+ const AddAttachmentSchema = z.object({
130
+ "card-id": z.string().min(1).describe("ID of the card to attach the file to"),
131
+ data: z.string().min(1).describe("Base64-encoded file data"),
132
+ filename: z
133
+ .string()
134
+ .min(1)
135
+ .describe("Filename with extension (e.g., 'image.png', 'audio.mp3')"),
136
+ "content-type": z
137
+ .string()
138
+ .optional()
139
+ .describe("MIME type of the file (e.g., 'image/png'). Can be inferred from filename if not provided."),
140
+ });
100
141
  const TemplateFieldSchema = z.object({
101
142
  id: z.string().describe("Unique identifier for the template field"),
102
143
  name: z.string().describe("Display name of the field"),
103
144
  pos: z.string().describe("Position of the field in the template"),
145
+ type: z
146
+ .string()
147
+ .optional()
148
+ .nullable()
149
+ .describe("Field type: null/text for user input, or ai/speech/translate/dictionary for auto-generated"),
150
+ source: z
151
+ .string()
152
+ .optional()
153
+ .nullable()
154
+ .describe("Source field ID for auto-generated fields"),
104
155
  options: z
105
156
  .object({
106
157
  "multi-line?": z
@@ -108,6 +159,7 @@ const TemplateFieldSchema = z.object({
108
159
  .optional()
109
160
  .describe("Whether the field supports multiple lines of text"),
110
161
  })
162
+ .passthrough()
111
163
  .optional()
112
164
  .describe("Additional options for the field"),
113
165
  });
@@ -158,7 +210,16 @@ const DeckSchema = z
158
210
  id: z.string().describe("Unique identifier for the deck"),
159
211
  sort: z.number().describe("Sort order of the deck"),
160
212
  name: z.string().describe("Display name of the deck"),
161
- archived: z.boolean().optional().describe("Whether the deck is archived"),
213
+ "archived?": z
214
+ .boolean()
215
+ .optional()
216
+ .nullable()
217
+ .describe("Whether the deck is archived"),
218
+ "trashed?": z
219
+ .object({ date: z.string() })
220
+ .optional()
221
+ .nullable()
222
+ .describe("Whether the deck is trashed"),
162
223
  })
163
224
  .strip();
164
225
  const ListDecksResponseSchema = z
@@ -167,6 +228,18 @@ const ListDecksResponseSchema = z
167
228
  docs: z.array(DeckSchema).describe("Array of decks"),
168
229
  })
169
230
  .strip();
231
+ const DueCardSchema = z
232
+ .object({
233
+ id: z.string().describe("Unique identifier for the card"),
234
+ content: z.string().describe("Markdown content of the card"),
235
+ name: z.string().describe("Display name of the card"),
236
+ "deck-id": z.string().describe("ID of the deck containing the card"),
237
+ "new?": z.boolean().describe("Whether the card is new (never reviewed)"),
238
+ })
239
+ .passthrough();
240
+ const GetDueCardsResponseSchema = z.object({
241
+ cards: z.array(DueCardSchema).describe("Array of cards due for review"),
242
+ });
170
243
  function getApiKey() {
171
244
  const apiKey = process.env.MOCHI_API_KEY;
172
245
  if (!apiKey) {
@@ -202,7 +275,7 @@ export class MochiClient {
202
275
  ? ListDecksParamsSchema.parse(params)
203
276
  : undefined;
204
277
  const response = await this.api.get("/decks", { params: validatedParams });
205
- return ListDecksResponseSchema.parse(response.data).docs.filter((deck) => !deck.archived);
278
+ return ListDecksResponseSchema.parse(response.data).docs.filter((deck) => !deck["archived?"] && !deck["trashed?"]);
206
279
  }
207
280
  async listCards(params) {
208
281
  const validatedParams = params
@@ -220,6 +293,94 @@ export class MochiClient {
220
293
  });
221
294
  return ListTemplatesResponseSchema.parse(response.data);
222
295
  }
296
+ async getDueCards(params) {
297
+ const validatedParams = params
298
+ ? GetDueCardsParamsSchema.parse(params)
299
+ : undefined;
300
+ const deckId = validatedParams?.["deck-id"];
301
+ const endpoint = deckId ? `/due/${deckId}` : "/due";
302
+ const queryParams = validatedParams?.date
303
+ ? { date: validatedParams.date }
304
+ : undefined;
305
+ const response = await this.api.get(endpoint, { params: queryParams });
306
+ return GetDueCardsResponseSchema.parse(response.data);
307
+ }
308
+ async getTemplate(templateId) {
309
+ const response = await this.api.get(`/templates/${templateId}`);
310
+ return TemplateSchema.parse(response.data);
311
+ }
312
+ async createCardFromTemplate(request) {
313
+ // Fetch the template to get field definitions
314
+ const template = await this.getTemplate(request["template-id"]);
315
+ // Map field names to IDs
316
+ const fieldNameToId = {};
317
+ for (const [fieldId, field] of Object.entries(template.fields)) {
318
+ fieldNameToId[field.name] = fieldId;
319
+ }
320
+ // Build the fields object with IDs
321
+ const fields = {};
322
+ const fieldValues = [];
323
+ for (const [fieldName, value] of Object.entries(request.fields)) {
324
+ const fieldId = fieldNameToId[fieldName];
325
+ if (!fieldId) {
326
+ throw new MochiError([
327
+ `Unknown field name: "${fieldName}". Available fields: ${Object.keys(fieldNameToId).join(", ")}`,
328
+ ], 400);
329
+ }
330
+ fields[fieldId] = { id: fieldId, value };
331
+ fieldValues.push(value);
332
+ }
333
+ // Build content from field values (joined with separator for multi-field templates)
334
+ const content = fieldValues.join("\n---\n");
335
+ const createRequest = {
336
+ content,
337
+ "deck-id": request["deck-id"],
338
+ "template-id": request["template-id"],
339
+ fields,
340
+ "manual-tags": request["manual-tags"],
341
+ };
342
+ return this.createCard(createRequest);
343
+ }
344
+ async addAttachment(request) {
345
+ // Infer content-type from filename if not provided
346
+ let contentType = request["content-type"];
347
+ if (!contentType) {
348
+ const ext = request.filename.split(".").pop()?.toLowerCase();
349
+ const mimeTypes = {
350
+ png: "image/png",
351
+ jpg: "image/jpeg",
352
+ jpeg: "image/jpeg",
353
+ gif: "image/gif",
354
+ webp: "image/webp",
355
+ svg: "image/svg+xml",
356
+ mp3: "audio/mpeg",
357
+ wav: "audio/wav",
358
+ ogg: "audio/ogg",
359
+ mp4: "video/mp4",
360
+ pdf: "application/pdf",
361
+ };
362
+ contentType = mimeTypes[ext ?? ""] ?? "application/octet-stream";
363
+ }
364
+ // Convert base64 to Buffer
365
+ const buffer = Buffer.from(request.data, "base64");
366
+ // Create form data
367
+ const formData = new FormData();
368
+ formData.append("file", buffer, {
369
+ filename: request.filename,
370
+ contentType,
371
+ });
372
+ // Upload attachment
373
+ await this.api.post(`/cards/${request["card-id"]}/attachments/${encodeURIComponent(request.filename)}`, formData, {
374
+ headers: {
375
+ ...formData.getHeaders(),
376
+ Authorization: `Basic ${Buffer.from(`${this.token}:`).toString("base64")}`,
377
+ },
378
+ });
379
+ return {
380
+ filename: request.filename,
381
+ markdown: `![](@media/${request.filename})`,
382
+ };
383
+ }
223
384
  }
224
385
  // Server setup
225
386
  const server = new Server({
@@ -236,7 +397,7 @@ const server = new Server({
236
397
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
237
398
  tools: [
238
399
  {
239
- name: "mochi_create_card",
400
+ name: "mochi_create_flashcard",
240
401
  description: `Create a new flashcard in Mochi. Use this whenever I ask questions about something that is interesting to remember. E.g. if I ask "What is the capital of France?", you should create a new flashcard with the content "What is the capital of France?\n---\nParis".
241
402
 
242
403
  ## Parameters
@@ -248,7 +409,7 @@ ALWAYS look up deck-id with the mochi_list_decks tool.
248
409
  The markdown content of the card. Separate front and back using a horizontal rule (---).
249
410
 
250
411
  ### template-id (optional)
251
- When using a template, the field ids MUST match the template ones. If not using a template, omit this field.
412
+ When using a template, the field ids MUST match the template ones. If not using a template, omit this field. Consider using mochi_create_card_from_template instead for easier template-based card creation.
252
413
 
253
414
  ### fields (optional)
254
415
  A map of field IDs (keyword) to field values. Only required when using a template. The field IDs must correspond to the fields defined on the template.
@@ -293,7 +454,57 @@ A map of field IDs (keyword) to field values. Only required when using a templat
293
454
  },
294
455
  },
295
456
  {
296
- name: "mochi_update_card",
457
+ name: "mochi_create_card_from_template",
458
+ description: `Create a flashcard using a template with field names (not IDs). This is the preferred way to create template-based cards.
459
+
460
+ The MCP automatically:
461
+ 1. Fetches the template to get field definitions
462
+ 2. Maps provided field names to their IDs
463
+ 3. Builds the fields object in the format Mochi expects
464
+
465
+ ## Parameters
466
+
467
+ ### template-id (required)
468
+ The ID of the template to use. Get available templates with mochi_list_templates.
469
+
470
+ ### deck-id (required)
471
+ The ID of the deck to create the card in. Get available decks with mochi_list_decks.
472
+
473
+ ### fields (required)
474
+ A map of field **names** (not IDs) to their values. The MCP will map names to IDs automatically.
475
+
476
+ ### manual-tags (optional)
477
+ Array of tags to add to the card.
478
+
479
+ ## Example: "Word" template (single field)
480
+ {
481
+ "template-id": "mzROLUuD",
482
+ "deck-id": "HGOW9dWP",
483
+ "fields": {
484
+ "Word": "serendipity"
485
+ }
486
+ }
487
+
488
+ ## Example: "Basic Flashcard" template (front/back)
489
+ {
490
+ "template-id": "Jyv52qHg",
491
+ "deck-id": "jJAIs2ZZ",
492
+ "fields": {
493
+ "Front": "What is the capital of France?",
494
+ "Back": "Paris"
495
+ }
496
+ }`,
497
+ inputSchema: zodToJsonSchema(CreateCardFromTemplateSchema),
498
+ annotations: {
499
+ title: "Create flashcard from template on Mochi",
500
+ readOnlyHint: false,
501
+ destructiveHint: false,
502
+ idempotentHint: false,
503
+ openWorldHint: true,
504
+ },
505
+ },
506
+ {
507
+ name: "mochi_update_flashcard",
297
508
  description: `Update or delete an existing flashcard in Mochi. To delete set trashed to true.`,
298
509
  inputSchema: zodToJsonSchema(z.object({
299
510
  "card-id": z.string(),
@@ -308,8 +519,42 @@ A map of field IDs (keyword) to field values. Only required when using a templat
308
519
  },
309
520
  },
310
521
  {
311
- name: "mochi_list_cards",
312
- description: "List cards in pages of 10 cards per page",
522
+ name: "mochi_add_attachment",
523
+ description: `Add an attachment (image, audio, etc.) to a card using base64 data.
524
+
525
+ Use this when you have base64-encoded file data (e.g., from images inserted in chat).
526
+
527
+ ## Parameters
528
+
529
+ ### card-id (required)
530
+ The ID of the card to attach the file to.
531
+
532
+ ### data (required)
533
+ Base64-encoded file data.
534
+
535
+ ### filename (required)
536
+ Filename with extension (e.g., "image.png", "audio.mp3").
537
+
538
+ ### content-type (optional)
539
+ MIME type (e.g., "image/png"). Can be inferred from filename.
540
+
541
+ ## Returns
542
+ The markdown reference to use in card content: \`![](@media/filename)\`
543
+
544
+ ## Note
545
+ For URL-based images, just use markdown directly in card content: \`![description](https://example.com/image.png)\` - no attachment upload needed.`,
546
+ inputSchema: zodToJsonSchema(AddAttachmentSchema),
547
+ annotations: {
548
+ title: "Add attachment to flashcard on Mochi",
549
+ readOnlyHint: false,
550
+ destructiveHint: false,
551
+ idempotentHint: false,
552
+ openWorldHint: true,
553
+ },
554
+ },
555
+ {
556
+ name: "mochi_list_flashcards",
557
+ description: "List flashcards in pages of 10 cards per page",
313
558
  inputSchema: zodToJsonSchema(ListCardsParamsSchema),
314
559
  annotations: {
315
560
  title: "List flashcards on Mochi",
@@ -333,7 +578,9 @@ A map of field IDs (keyword) to field values. Only required when using a templat
333
578
  },
334
579
  {
335
580
  name: "mochi_list_templates",
336
- description: `Templates can be used to create cards with pre-defined fields using the template_id field.
581
+ description: `List all templates. Templates can be used to create cards with pre-defined fields.
582
+
583
+ Use mochi_create_card_from_template for easy template-based card creation with field names instead of IDs.
337
584
 
338
585
  Example response:
339
586
  {
@@ -372,6 +619,26 @@ Example response:
372
619
  openWorldHint: false,
373
620
  },
374
621
  },
622
+ {
623
+ name: "mochi_get_due_cards",
624
+ description: `Get flashcards that are due for review. Returns cards scheduled for review on the specified date (defaults to today).
625
+
626
+ ## Parameters
627
+
628
+ ### deck-id (optional)
629
+ Filter to only show due cards from a specific deck.
630
+
631
+ ### date (optional)
632
+ ISO 8601 date string (e.g., "2026-01-11T00:00:00.000Z") to get cards due on that date. Defaults to today.`,
633
+ inputSchema: zodToJsonSchema(GetDueCardsParamsSchema),
634
+ annotations: {
635
+ title: "Get due flashcards on Mochi",
636
+ readOnlyHint: true,
637
+ destructiveHint: false,
638
+ idempotentHint: true,
639
+ openWorldHint: false,
640
+ },
641
+ },
375
642
  ],
376
643
  }));
377
644
  // Create Mochi client
@@ -408,7 +675,7 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
408
675
  text: JSON.stringify(decks.map((deck) => ({
409
676
  id: deck.id,
410
677
  name: deck.name,
411
- archived: deck.archived,
678
+ archived: deck["archived?"],
412
679
  })), null, 2),
413
680
  },
414
681
  ],
@@ -477,7 +744,7 @@ Input: ${input}
477
744
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
478
745
  try {
479
746
  switch (request.params.name) {
480
- case "mochi_create_card": {
747
+ case "mochi_create_flashcard": {
481
748
  const validatedArgs = CreateCardRequestSchema.parse(request.params.arguments);
482
749
  const response = await mochiClient.createCard(validatedArgs);
483
750
  return {
@@ -490,7 +757,20 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
490
757
  isError: false,
491
758
  };
492
759
  }
493
- case "mochi_update_card": {
760
+ case "mochi_create_card_from_template": {
761
+ const validatedArgs = CreateCardFromTemplateSchema.parse(request.params.arguments);
762
+ const response = await mochiClient.createCardFromTemplate(validatedArgs);
763
+ return {
764
+ content: [
765
+ {
766
+ type: "text",
767
+ text: JSON.stringify(response, null, 2),
768
+ },
769
+ ],
770
+ isError: false,
771
+ };
772
+ }
773
+ case "mochi_update_flashcard": {
494
774
  const { "card-id": cardId, ...updateArgs } = z
495
775
  .object({
496
776
  "card-id": z.string(),
@@ -508,6 +788,19 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
508
788
  isError: false,
509
789
  };
510
790
  }
791
+ case "mochi_add_attachment": {
792
+ const validatedArgs = AddAttachmentSchema.parse(request.params.arguments);
793
+ const response = await mochiClient.addAttachment(validatedArgs);
794
+ return {
795
+ content: [
796
+ {
797
+ type: "text",
798
+ text: JSON.stringify(response, null, 2),
799
+ },
800
+ ],
801
+ isError: false,
802
+ };
803
+ }
511
804
  case "mochi_list_decks": {
512
805
  const validatedArgs = ListDecksParamsSchema.parse(request.params.arguments);
513
806
  const response = await mochiClient.listDecks(validatedArgs);
@@ -521,7 +814,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
521
814
  isError: false,
522
815
  };
523
816
  }
524
- case "mochi_list_cards": {
817
+ case "mochi_list_flashcards": {
525
818
  const validatedArgs = ListCardsParamsSchema.parse(request.params.arguments);
526
819
  const response = await mochiClient.listCards(validatedArgs);
527
820
  return {
@@ -547,6 +840,19 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
547
840
  isError: false,
548
841
  };
549
842
  }
843
+ case "mochi_get_due_cards": {
844
+ const validatedArgs = GetDueCardsParamsSchema.parse(request.params.arguments);
845
+ const response = await mochiClient.getDueCards(validatedArgs);
846
+ return {
847
+ content: [
848
+ {
849
+ type: "text",
850
+ text: JSON.stringify(response, null, 2),
851
+ },
852
+ ],
853
+ isError: false,
854
+ };
855
+ }
550
856
  default:
551
857
  return {
552
858
  content: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fredrika/mcp-mochi",
3
- "version": "1.0.5",
3
+ "version": "1.0.6-beta.0",
4
4
  "description": "MCP server for Mochi flashcard integration",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -32,6 +32,7 @@
32
32
  "@modelcontextprotocol/sdk": "^1.9.0",
33
33
  "axios": "^1.6.7",
34
34
  "dotenv": "^16.4.5",
35
+ "form-data": "^4.0.5",
35
36
  "zod": "^3.22.4"
36
37
  },
37
38
  "devDependencies": {