@fredrika/mcp-mochi 1.0.6-beta.1 → 2.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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Mochi MCP Server
2
2
 
3
- This MCP server provides integration with the Mochi flashcard system, allowing you to manage your flashcards through the Model Context Protocol.
3
+ MCP server for [Mochi](https://mochi.cards) flashcard integration, allowing you to manage your flashcards through the Model Context Protocol.
4
4
 
5
5
  ## Features
6
6
 
@@ -67,51 +67,32 @@ Add the following to your `claude_desktop_config.json`:
67
67
 
68
68
  ## Available Tools
69
69
 
70
- ### `mochi_create_flashcard`
71
- Create a new flashcard in Mochi.
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
102
-
103
- ### `mochi_list_decks`
104
- List all decks.
105
- - `bookmark`: (optional) Pagination token
106
-
107
- ### `mochi_list_templates`
108
- List all templates with their field definitions.
109
- - `bookmark`: (optional) Pagination token
110
-
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)
70
+ | Tool | Description |
71
+ |------|-------------|
72
+ | `mochi_create_flashcard` | Create a new flashcard in Mochi |
73
+ | `mochi_create_card_from_template` | Create a flashcard using a template with field names (auto-maps to IDs) |
74
+ | `mochi_update_flashcard` | Update a flashcard's content, deck, template, or fields. Can also soft-delete with `trashed` property |
75
+ | `mochi_delete_flashcard` | Permanently delete a flashcard and its attachments (cannot be undone) |
76
+ | `mochi_archive_flashcard` | Archive or unarchive a flashcard |
77
+ | `mochi_add_attachment` | Add an attachment (image, audio, etc.) to a card using base64 data |
78
+ | `mochi_list_flashcards` | List flashcards, optionally filtered by deck |
79
+ | `mochi_list_decks` | List all decks |
80
+ | `mochi_list_templates` | List all templates with their field definitions |
81
+ | `mochi_get_template` | Get a single template by ID |
82
+ | `mochi_get_due_cards` | Get flashcards due for review |
83
+
84
+ ## Resources
85
+
86
+ | URI | Description |
87
+ |-----|-------------|
88
+ | `mochi://decks` | List of all decks |
89
+ | `mochi://templates` | List of all templates |
90
+
91
+ ## Prompts
92
+
93
+ | Prompt | Description |
94
+ |--------|-------------|
95
+ | `write-flashcard` | Generates a well-structured flashcard following best practices (atomic questions, cloze deletions, etc.) |
115
96
 
116
97
  ## Examples
117
98
 
@@ -122,7 +103,7 @@ Get flashcards due for review.
122
103
  "tool": "mochi_create_flashcard",
123
104
  "params": {
124
105
  "content": "What is MCP?\n---\nModel Context Protocol - a protocol for providing context to LLMs",
125
- "deck-id": "<DECK_ID>"
106
+ "deckId": "<DECK_ID>"
126
107
  }
127
108
  }
128
109
  ```
@@ -133,8 +114,8 @@ Get flashcards due for review.
133
114
  {
134
115
  "tool": "mochi_create_card_from_template",
135
116
  "params": {
136
- "template-id": "<TEMPLATE_ID>",
137
- "deck-id": "<DECK_ID>",
117
+ "templateId": "<TEMPLATE_ID>",
118
+ "deckId": "<DECK_ID>",
138
119
  "fields": {
139
120
  "Front": "What is the capital of France?",
140
121
  "Back": "Paris"
package/dist/index.d.ts CHANGED
@@ -112,7 +112,7 @@ declare const ListCardsResponseSchema: z.ZodObject<{
112
112
  }, z.core.$strip>>;
113
113
  }, z.core.$strip>;
114
114
  type CreateCardResponse = z.infer<typeof CreateCardResponseSchema>;
115
- type ListDecksResponse = z.infer<typeof ListDecksResponseSchema>["docs"];
115
+ type ListDecksResponse = z.infer<typeof ListDecksResponseSchema>;
116
116
  type ListCardsResponse = z.infer<typeof ListCardsResponseSchema>;
117
117
  declare const ListDecksResponseSchema: z.ZodObject<{
118
118
  bookmark: z.ZodString;
@@ -120,6 +120,7 @@ declare const ListDecksResponseSchema: z.ZodObject<{
120
120
  id: z.ZodString;
121
121
  sort: z.ZodNumber;
122
122
  name: z.ZodString;
123
+ "template-id": z.ZodNullable<z.ZodOptional<z.ZodString>>;
123
124
  "archived?": z.ZodNullable<z.ZodOptional<z.ZodBoolean>>;
124
125
  "trashed?": z.ZodNullable<z.ZodOptional<z.ZodObject<{
125
126
  date: z.ZodString;
@@ -132,7 +133,7 @@ declare const GetDueCardsResponseSchema: z.ZodObject<{
132
133
  content: z.ZodString;
133
134
  name: z.ZodString;
134
135
  "deck-id": z.ZodString;
135
- "new?": z.ZodBoolean;
136
+ "new?": z.ZodOptional<z.ZodBoolean>;
136
137
  }, z.core.$loose>>;
137
138
  }, z.core.$strip>;
138
139
  export declare class MochiClient {
@@ -147,6 +148,7 @@ export declare class MochiClient {
147
148
  getDueCards(params?: GetDueCardsParams): Promise<z.infer<typeof GetDueCardsResponseSchema>>;
148
149
  getTemplate(templateId: string): Promise<z.infer<typeof TemplateSchema>>;
149
150
  createCardFromTemplate(request: CreateCardFromTemplateParams): Promise<CreateCardResponse>;
151
+ deleteCard(cardId: string): Promise<void>;
150
152
  addAttachment(request: AddAttachmentRequest): Promise<{
151
153
  filename: string;
152
154
  markdown: string;
package/dist/index.js CHANGED
@@ -90,6 +90,9 @@ const ListTemplatesParamsSchema = z.object({
90
90
  .optional()
91
91
  .describe("Pagination bookmark for fetching next page of results"),
92
92
  });
93
+ const GetTemplateParamsSchema = z.object({
94
+ templateId: z.string().min(1).describe("ID of the template to fetch"),
95
+ });
93
96
  const GetDueCardsParamsSchema = z.object({
94
97
  deckId: z
95
98
  .string()
@@ -239,6 +242,11 @@ const DeckSchema = z
239
242
  id: z.string().describe("Unique identifier for the deck"),
240
243
  sort: z.number().describe("Sort order of the deck"),
241
244
  name: z.string().describe("Display name of the deck"),
245
+ "template-id": z
246
+ .string()
247
+ .optional()
248
+ .nullable()
249
+ .describe("Template ID associated with this deck, if any"),
242
250
  "archived?": z
243
251
  .boolean()
244
252
  .optional()
@@ -248,7 +256,7 @@ const DeckSchema = z
248
256
  .object({ date: z.string() })
249
257
  .optional()
250
258
  .nullable()
251
- .describe("Whether the deck is trashed"),
259
+ .describe("Timestamp when the deck was trashed, in ISO 8601 format (matching JavaScript's Date#toJSON)"),
252
260
  })
253
261
  .strip();
254
262
  const ListDecksResponseSchema = z
@@ -263,7 +271,10 @@ const DueCardSchema = z
263
271
  content: z.string().describe("Markdown content of the card"),
264
272
  name: z.string().describe("Display name of the card"),
265
273
  "deck-id": z.string().describe("ID of the deck containing the card"),
266
- "new?": z.boolean().describe("Whether the card is new (never reviewed)"),
274
+ "new?": z
275
+ .boolean()
276
+ .optional()
277
+ .describe("Whether the card is new (never reviewed)"),
267
278
  })
268
279
  .passthrough();
269
280
  const GetDueCardsResponseSchema = z.object({
@@ -306,9 +317,13 @@ export class MochiClient {
306
317
  ? ListDecksParamsSchema.parse(params)
307
318
  : undefined;
308
319
  const response = await this.api.get("/decks", { params: validatedParams });
309
- return ListDecksResponseSchema.parse(response.data)
310
- .docs.filter((deck) => !deck["archived?"] && !deck["trashed?"])
311
- .sort((a, b) => a.sort - b.sort);
320
+ const parsed = ListDecksResponseSchema.parse(response.data);
321
+ return {
322
+ bookmark: parsed.bookmark,
323
+ docs: parsed.docs
324
+ .filter((deck) => !deck["archived?"] && !deck["trashed?"])
325
+ .sort((a, b) => a.sort - b.sort),
326
+ };
312
327
  }
313
328
  async listCards(params) {
314
329
  const validatedParams = params
@@ -377,6 +392,9 @@ export class MochiClient {
377
392
  };
378
393
  return this.createCard(createRequest);
379
394
  }
395
+ async deleteCard(cardId) {
396
+ await this.api.delete(`/cards/${cardId}`);
397
+ }
380
398
  async addAttachment(request) {
381
399
  // Infer content-type from filename if not provided
382
400
  let contentType = request.contentType;
@@ -426,7 +444,34 @@ const server = new McpServer({
426
444
  // Schema for update flashcard tool (combines cardId with update fields)
427
445
  const UpdateFlashcardToolSchema = z.object({
428
446
  cardId: z.string().describe("ID of the card to update"),
429
- ...UpdateCardRequestSchema.shape,
447
+ content: z
448
+ .string()
449
+ .optional()
450
+ .describe("Updated markdown content of the card"),
451
+ deckId: z.string().optional().describe("ID of the deck to move the card to"),
452
+ templateId: z.string().optional().describe("Template ID to use for the card"),
453
+ fields: z
454
+ .record(z.string(), CreateCardFieldSchema)
455
+ .optional()
456
+ .describe("Updated map of field IDs to field values"),
457
+ trashed: z
458
+ .boolean()
459
+ .optional()
460
+ .describe("Set to true to soft-delete (move to trash). This can be undone by setting to false."),
461
+ });
462
+ // Schema for delete flashcard tool
463
+ const DeleteFlashcardToolSchema = z.object({
464
+ cardId: z
465
+ .string()
466
+ .describe("ID of the card to permanently delete. This cannot be undone."),
467
+ });
468
+ // Schema for archive flashcard tool
469
+ const ArchiveFlashcardToolSchema = z.object({
470
+ cardId: z.string().describe("ID of the card to archive"),
471
+ archived: z
472
+ .boolean()
473
+ .default(true)
474
+ .describe("Set to true to archive, false to unarchive"),
430
475
  });
431
476
  // Output schema for attachment response
432
477
  const AddAttachmentResponseSchema = z.object({
@@ -482,7 +527,7 @@ function formatToolError(error) {
482
527
  // Note: Using type assertions due to Zod version compatibility between SDK (v4) and project (v3)
483
528
  server.registerTool("mochi_create_flashcard", {
484
529
  title: "Create flashcard on Mochi",
485
- description: "Create a new flashcard in Mochi. Look up deckId with mochi_list_decks first. For template-based cards, prefer mochi_create_card_from_template.",
530
+ description: "Create a new flashcard in Mochi. Look up deckId with mochi_list_decks first. Use mochi_create_card_from_template if the deck has a template-id defined.",
486
531
  inputSchema: CreateCardRequestSchema,
487
532
  outputSchema: CreateCardResponseSchema,
488
533
  annotations: {
@@ -505,7 +550,7 @@ server.registerTool("mochi_create_flashcard", {
505
550
  });
506
551
  server.registerTool("mochi_create_card_from_template", {
507
552
  title: "Create flashcard from template on Mochi",
508
- description: "Create a flashcard using a template with field names (not IDs). Preferred way to create template-based cards. Automatically maps field names to IDs. Get templates with mochi_list_templates, decks with mochi_list_decks.",
553
+ description: "Create a flashcard using a template with field names (not IDs). Automatically maps field names to IDs. Get templates with mochi_list_templates, decks with mochi_list_decks.",
509
554
  inputSchema: CreateCardFromTemplateSchema,
510
555
  outputSchema: CreateCardResponseSchema,
511
556
  annotations: {
@@ -528,7 +573,7 @@ server.registerTool("mochi_create_card_from_template", {
528
573
  });
529
574
  server.registerTool("mochi_update_flashcard", {
530
575
  title: "Update flashcard on Mochi",
531
- description: "Update or delete an existing flashcard. Set trashed to true to delete.",
576
+ description: "Update an existing flashcard's content, deck, template, or fields. Use mochi_delete_flashcard to delete or mochi_archive_flashcard to archive.",
532
577
  inputSchema: UpdateFlashcardToolSchema,
533
578
  outputSchema: UpdateCardResponseSchema,
534
579
  annotations: {
@@ -550,6 +595,62 @@ server.registerTool("mochi_update_flashcard", {
550
595
  return formatToolError(error);
551
596
  }
552
597
  });
598
+ // Output schema for delete response
599
+ const DeleteFlashcardResponseSchema = z
600
+ .object({
601
+ success: z.boolean().describe("Whether the deletion was successful"),
602
+ cardId: z.string().describe("ID of the deleted card"),
603
+ })
604
+ .strict();
605
+ server.registerTool("mochi_delete_flashcard", {
606
+ title: "Delete flashcard on Mochi",
607
+ description: "Permanently delete a flashcard and its attachments. WARNING: This cannot be undone. For soft deletion, use mochi_update_flashcard with trashed: true.",
608
+ inputSchema: DeleteFlashcardToolSchema,
609
+ outputSchema: DeleteFlashcardResponseSchema,
610
+ annotations: {
611
+ readOnlyHint: false,
612
+ destructiveHint: true,
613
+ idempotentHint: true,
614
+ openWorldHint: true,
615
+ },
616
+ }, async (args) => {
617
+ try {
618
+ await mochiClient.deleteCard(args.cardId);
619
+ const response = { success: true, cardId: args.cardId };
620
+ return {
621
+ content: [{ type: "text", text: JSON.stringify(response, null, 2) }],
622
+ structuredContent: response,
623
+ };
624
+ }
625
+ catch (error) {
626
+ return formatToolError(error);
627
+ }
628
+ });
629
+ server.registerTool("mochi_archive_flashcard", {
630
+ title: "Archive flashcard on Mochi",
631
+ description: "Archive or unarchive a flashcard. Archived cards are hidden from review but not deleted.",
632
+ inputSchema: ArchiveFlashcardToolSchema,
633
+ outputSchema: UpdateCardResponseSchema,
634
+ annotations: {
635
+ readOnlyHint: false,
636
+ destructiveHint: false,
637
+ idempotentHint: true,
638
+ openWorldHint: true,
639
+ },
640
+ }, async (args) => {
641
+ try {
642
+ const response = await mochiClient.updateCard(args.cardId, {
643
+ archived: args.archived,
644
+ });
645
+ return {
646
+ content: [{ type: "text", text: JSON.stringify(response, null, 2) }],
647
+ structuredContent: response,
648
+ };
649
+ }
650
+ catch (error) {
651
+ return formatToolError(error);
652
+ }
653
+ });
553
654
  server.registerTool("mochi_add_attachment", {
554
655
  title: "Add attachment to flashcard on Mochi",
555
656
  description: "Add an attachment (image, audio, etc.) to a card using base64 data. For URL-based images, just use markdown directly in card content instead.",
@@ -612,7 +713,7 @@ server.registerTool("mochi_list_decks", {
612
713
  const response = await mochiClient.listDecks(args);
613
714
  return {
614
715
  content: [{ type: "text", text: JSON.stringify(response, null, 2) }],
615
- structuredContent: { bookmark: "", docs: response },
716
+ structuredContent: response,
616
717
  };
617
718
  }
618
719
  catch (error) {
@@ -642,6 +743,29 @@ server.registerTool("mochi_list_templates", {
642
743
  return formatToolError(error);
643
744
  }
644
745
  });
746
+ server.registerTool("mochi_get_template", {
747
+ title: "Get template by ID on Mochi",
748
+ description: "Get a single template by its ID. Use to see template fields and structure.",
749
+ inputSchema: GetTemplateParamsSchema.shape,
750
+ outputSchema: TemplateSchema,
751
+ annotations: {
752
+ readOnlyHint: true,
753
+ destructiveHint: false,
754
+ idempotentHint: true,
755
+ openWorldHint: false,
756
+ },
757
+ }, async (args) => {
758
+ try {
759
+ const response = await mochiClient.getTemplate(args.templateId);
760
+ return {
761
+ content: [{ type: "text", text: JSON.stringify(response, null, 2) }],
762
+ structuredContent: response,
763
+ };
764
+ }
765
+ catch (error) {
766
+ return formatToolError(error);
767
+ }
768
+ });
645
769
  server.registerTool("mochi_get_due_cards", {
646
770
  title: "Get due flashcards on Mochi",
647
771
  description: "Get flashcards due for review on a specific date (defaults to today).",
@@ -670,13 +794,13 @@ server.registerResource("decks", "mochi://decks", {
670
794
  description: "List of all decks in Mochi.",
671
795
  mimeType: "application/json",
672
796
  }, async () => {
673
- const decks = await mochiClient.listDecks();
797
+ const response = await mochiClient.listDecks();
674
798
  return {
675
799
  contents: [
676
800
  {
677
801
  uri: "mochi://decks",
678
802
  mimeType: "application/json",
679
- text: JSON.stringify(decks.map((deck) => ({ id: deck.id, name: deck.name })), null, 2),
803
+ text: JSON.stringify(response.docs.map((deck) => ({ id: deck.id, name: deck.name })), null, 2),
680
804
  },
681
805
  ],
682
806
  };
@@ -716,6 +840,7 @@ server.registerPrompt("write-flashcard", {
716
840
  - Utilize cloze prompts when applicable, like "This is a text with {{hidden}} part. Then don't use '---' separator.".
717
841
  - Focus on effective retrieval practice by being concise and clear.
718
842
  - Make it just challenging enough to reinforce specific facts.
843
+ - Only use mochi_create_card_from_template if the deck has a template-id defined. Otherwise use mochi_create_flashcard.
719
844
  Input: ${input}
720
845
  `,
721
846
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fredrika/mcp-mochi",
3
- "version": "1.0.6-beta.1",
3
+ "version": "2.0.0",
4
4
  "description": "MCP server for Mochi flashcard integration",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",