@fredrika/mcp-mochi 1.0.6-beta.0 → 1.0.6-beta.1

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/dist/index.js CHANGED
@@ -1,12 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
3
2
  import axios from "axios";
4
3
  import FormData from "form-data";
5
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
6
  import dotenv from "dotenv";
8
7
  import { z } from "zod";
9
- import { zodToJsonSchema } from "zod-to-json-schema";
10
8
  dotenv.config();
11
9
  /**
12
10
  * Custom error class for Mochi API errors
@@ -37,14 +35,14 @@ const CreateCardRequestSchema = z.object({
37
35
  .string()
38
36
  .min(1)
39
37
  .describe("Markdown content of the card. Separate front and back using a horizontal rule (---) or use brackets for {{cloze deletion}}."),
40
- "deck-id": z.string().min(1).describe("ID of the deck to create the card in"),
41
- "template-id": z
38
+ deckId: z.string().min(1).describe("ID of the deck to create the card in"),
39
+ templateId: z
42
40
  .string()
43
41
  .optional()
44
42
  .nullable()
45
43
  .default(null)
46
44
  .describe("Optional template ID to use for the card. Defaults to null if not set."),
47
- "manual-tags": z
45
+ tags: z
48
46
  .array(z.string())
49
47
  .optional()
50
48
  .describe("Optional array of tags to add to the card"),
@@ -58,16 +56,10 @@ const UpdateCardRequestSchema = z.object({
58
56
  .string()
59
57
  .optional()
60
58
  .describe("Updated markdown content of the card"),
61
- "deck-id": z
62
- .string()
63
- .optional()
64
- .describe("ID of the deck to move the card to"),
65
- "template-id": z
66
- .string()
67
- .optional()
68
- .describe("Template ID to use for the card"),
69
- "archived?": z.boolean().optional().describe("Whether the card is archived"),
70
- "trashed?": z.string().optional().describe("Whether the card is trashed"),
59
+ deckId: z.string().optional().describe("ID of the deck to move the card to"),
60
+ templateId: z.string().optional().describe("Template ID to use for the card"),
61
+ archived: z.boolean().optional().describe("Whether the card is archived"),
62
+ trashed: z.boolean().optional().describe("Whether the card is trashed"),
71
63
  fields: z
72
64
  .record(z.string(), CreateCardFieldSchema)
73
65
  .optional()
@@ -80,7 +72,7 @@ const ListDecksParamsSchema = z.object({
80
72
  .describe("Pagination bookmark for fetching next page of results"),
81
73
  });
82
74
  const ListCardsParamsSchema = z.object({
83
- "deck-id": z.string().optional().describe("Get cards from deck ID"),
75
+ deckId: z.string().optional().describe("Get cards from deck ID"),
84
76
  limit: z
85
77
  .number()
86
78
  .min(1)
@@ -99,7 +91,7 @@ const ListTemplatesParamsSchema = z.object({
99
91
  .describe("Pagination bookmark for fetching next page of results"),
100
92
  });
101
93
  const GetDueCardsParamsSchema = z.object({
102
- "deck-id": z
94
+ deckId: z
103
95
  .string()
104
96
  .optional()
105
97
  .describe("Optional deck ID to filter due cards by a specific deck"),
@@ -109,35 +101,71 @@ const GetDueCardsParamsSchema = z.object({
109
101
  .describe("Optional ISO 8601 date to get cards due on that date. Defaults to today."),
110
102
  });
111
103
  const CreateCardFromTemplateSchema = z.object({
112
- "template-id": z
104
+ templateId: z
113
105
  .string()
114
106
  .min(1)
115
107
  .describe("ID of the template to use. Get this from mochi_list_templates."),
116
- "deck-id": z
108
+ deckId: z
117
109
  .string()
118
110
  .min(1)
119
111
  .describe("ID of the deck to create the card in. Get this from mochi_list_decks."),
120
112
  fields: z
121
113
  .record(z.string(), z.string())
122
114
  .describe('Map of field NAMES (not IDs) to values. E.g., { "Word": "serendipity" } or { "Front": "Question?", "Back": "Answer" }'),
123
- "manual-tags": z
115
+ tags: z
124
116
  .array(z.string())
125
117
  .optional()
126
118
  .describe("Optional array of tags to add to the card"),
127
119
  });
128
120
  // Schema for adding attachments
129
121
  const AddAttachmentSchema = z.object({
130
- "card-id": z.string().min(1).describe("ID of the card to attach the file to"),
122
+ cardId: z.string().min(1).describe("ID of the card to attach the file to"),
131
123
  data: z.string().min(1).describe("Base64-encoded file data"),
132
124
  filename: z
133
125
  .string()
134
126
  .min(1)
135
127
  .describe("Filename with extension (e.g., 'image.png', 'audio.mp3')"),
136
- "content-type": z
128
+ contentType: z
137
129
  .string()
138
130
  .optional()
139
131
  .describe("MIME type of the file (e.g., 'image/png'). Can be inferred from filename if not provided."),
140
132
  });
133
+ // Helper to transform camelCase params to hyphenated format for Mochi API
134
+ function toMochiCreateCardRequest(params) {
135
+ return {
136
+ content: params.content,
137
+ "deck-id": params.deckId,
138
+ "template-id": params.templateId,
139
+ "manual-tags": params.tags,
140
+ fields: params.fields,
141
+ };
142
+ }
143
+ function toMochiUpdateCardRequest(params) {
144
+ const result = {};
145
+ if (params.content !== undefined)
146
+ result.content = params.content;
147
+ if (params.deckId !== undefined)
148
+ result["deck-id"] = params.deckId;
149
+ if (params.templateId !== undefined)
150
+ result["template-id"] = params.templateId;
151
+ if (params.archived !== undefined)
152
+ result["archived?"] = params.archived;
153
+ if (params.trashed !== undefined)
154
+ result["trashed?"] = params.trashed;
155
+ if (params.fields !== undefined)
156
+ result.fields = params.fields;
157
+ return result;
158
+ }
159
+ function toMochiListCardsParams(params) {
160
+ const result = {};
161
+ if (params.deckId !== undefined)
162
+ result["deck-id"] = params.deckId;
163
+ if (params.limit !== undefined)
164
+ result.limit = params.limit;
165
+ if (params.bookmark !== undefined)
166
+ result.bookmark = params.bookmark;
167
+ return result;
168
+ }
141
169
  const TemplateFieldSchema = z.object({
142
170
  id: z.string().describe("Unique identifier for the template field"),
143
171
  name: z.string().describe("Display name of the field"),
@@ -193,12 +221,13 @@ const CardSchema = z
193
221
  name: z.string().describe("Display name of the card"),
194
222
  "deck-id": z.string().describe("ID of the deck containing the card"),
195
223
  fields: z
196
- .record(z.unknown())
224
+ .record(z.string(), z.unknown())
197
225
  .optional()
198
226
  .describe("Map of field IDs to field values. Need to match the field IDs in the template"),
199
227
  })
200
228
  .strip();
201
229
  const CreateCardResponseSchema = CardSchema.strip();
230
+ const UpdateCardResponseSchema = CardSchema.strip();
202
231
  const ListCardsResponseSchema = z
203
232
  .object({
204
233
  bookmark: z.string().describe("Pagination bookmark for fetching next page"),
@@ -263,11 +292,13 @@ export class MochiClient {
263
292
  });
264
293
  }
265
294
  async createCard(request) {
266
- const response = await this.api.post("/cards", request);
295
+ const mochiRequest = toMochiCreateCardRequest(request);
296
+ const response = await this.api.post("/cards", mochiRequest);
267
297
  return CreateCardResponseSchema.parse(response.data);
268
298
  }
269
299
  async updateCard(cardId, request) {
270
- const response = await this.api.post(`/cards/${cardId}`, request);
300
+ const mochiRequest = toMochiUpdateCardRequest(request);
301
+ const response = await this.api.post(`/cards/${cardId}`, mochiRequest);
271
302
  return CreateCardResponseSchema.parse(response.data);
272
303
  }
273
304
  async listDecks(params) {
@@ -275,13 +306,18 @@ export class MochiClient {
275
306
  ? ListDecksParamsSchema.parse(params)
276
307
  : undefined;
277
308
  const response = await this.api.get("/decks", { params: validatedParams });
278
- return ListDecksResponseSchema.parse(response.data).docs.filter((deck) => !deck["archived?"] && !deck["trashed?"]);
309
+ return ListDecksResponseSchema.parse(response.data)
310
+ .docs.filter((deck) => !deck["archived?"] && !deck["trashed?"])
311
+ .sort((a, b) => a.sort - b.sort);
279
312
  }
280
313
  async listCards(params) {
281
314
  const validatedParams = params
282
315
  ? ListCardsParamsSchema.parse(params)
283
316
  : undefined;
284
- const response = await this.api.get("/cards", { params: validatedParams });
317
+ const mochiParams = validatedParams
318
+ ? toMochiListCardsParams(validatedParams)
319
+ : undefined;
320
+ const response = await this.api.get("/cards", { params: mochiParams });
285
321
  return ListCardsResponseSchema.parse(response.data);
286
322
  }
287
323
  async listTemplates(params) {
@@ -297,7 +333,7 @@ export class MochiClient {
297
333
  const validatedParams = params
298
334
  ? GetDueCardsParamsSchema.parse(params)
299
335
  : undefined;
300
- const deckId = validatedParams?.["deck-id"];
336
+ const deckId = validatedParams?.deckId;
301
337
  const endpoint = deckId ? `/due/${deckId}` : "/due";
302
338
  const queryParams = validatedParams?.date
303
339
  ? { date: validatedParams.date }
@@ -311,7 +347,7 @@ export class MochiClient {
311
347
  }
312
348
  async createCardFromTemplate(request) {
313
349
  // Fetch the template to get field definitions
314
- const template = await this.getTemplate(request["template-id"]);
350
+ const template = await this.getTemplate(request.templateId);
315
351
  // Map field names to IDs
316
352
  const fieldNameToId = {};
317
353
  for (const [fieldId, field] of Object.entries(template.fields)) {
@@ -334,16 +370,16 @@ export class MochiClient {
334
370
  const content = fieldValues.join("\n---\n");
335
371
  const createRequest = {
336
372
  content,
337
- "deck-id": request["deck-id"],
338
- "template-id": request["template-id"],
373
+ deckId: request.deckId,
374
+ templateId: request.templateId,
339
375
  fields,
340
- "manual-tags": request["manual-tags"],
376
+ tags: request.tags,
341
377
  };
342
378
  return this.createCard(createRequest);
343
379
  }
344
380
  async addAttachment(request) {
345
381
  // Infer content-type from filename if not provided
346
- let contentType = request["content-type"];
382
+ let contentType = request.contentType;
347
383
  if (!contentType) {
348
384
  const ext = request.filename.split(".").pop()?.toLowerCase();
349
385
  const mimeTypes = {
@@ -370,7 +406,7 @@ export class MochiClient {
370
406
  contentType,
371
407
  });
372
408
  // Upload attachment
373
- await this.api.post(`/cards/${request["card-id"]}/attachments/${encodeURIComponent(request.filename)}`, formData, {
409
+ await this.api.post(`/cards/${request.cardId}/attachments/${encodeURIComponent(request.filename)}`, formData, {
374
410
  headers: {
375
411
  ...formData.getHeaders(),
376
412
  Authorization: `Basic ${Buffer.from(`${this.token}:`).toString("base64")}`,
@@ -383,529 +419,309 @@ export class MochiClient {
383
419
  }
384
420
  }
385
421
  // Server setup
386
- const server = new Server({
422
+ const server = new McpServer({
387
423
  name: "mcp-server/mochi",
388
424
  version: "1.0.3",
389
- }, {
390
- capabilities: {
391
- tools: {},
392
- resources: {},
393
- prompts: {},
394
- },
395
425
  });
396
- // Set up request handlers
397
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
398
- tools: [
399
- {
400
- name: "mochi_create_flashcard",
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".
402
-
403
- ## Parameters
404
-
405
- ### deck-id (required)
406
- ALWAYS look up deck-id with the mochi_list_decks tool.
407
-
408
- ### content (required)
409
- The markdown content of the card. Separate front and back using a horizontal rule (---).
410
-
411
- ### template-id (optional)
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.
413
-
414
- ### fields (optional)
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.
416
-
417
- ## Example without template
418
- {
419
- "content": "What is the capital of France?\n---\nParis",
420
- "deck-id": "btmZUXWM"
421
- }
422
-
423
- ## Example with template
424
- {
425
- "content": "New card from API. ![](@media/foobar03.png)",
426
- "deck-id": "btmZUXWM",
427
- "template-id": "8BtaEAXe",
428
- "fields": {
429
- "name": {
430
- "id": "name",
431
- "value": "Hello,"
432
- },
433
- "JNEnw1e7": {
434
- "id": "JNEnw1e7",
435
- "value": "World!"
436
- }
437
- }
438
- }
439
-
440
- ## Properties of good flashcards:
441
- - **focused:** A question or answer involving too much detail will dull your concentration and stimulate incomplete retrievals, leaving some bulbs unlit.
442
- - **precise** about what they're asking for. Vague questions will elicit vague answers, which won't reliably light the bulbs you're targeting.
443
- - **consistent** answers, lighting the same bulbs each time you perform the task.
444
- - **tractable**: Write prompts which you can almost always answer correctly. This often means breaking the task down, or adding cues
445
- - **effortful**: You shouldn't be able to trivially infer the answer.
446
- `,
447
- inputSchema: zodToJsonSchema(CreateCardRequestSchema),
448
- annotations: {
449
- title: "Create flashcard on Mochi",
450
- readOnlyHint: false,
451
- destructiveHint: false,
452
- idempotentHint: false,
453
- openWorldHint: true,
454
- },
455
- },
456
- {
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",
508
- description: `Update or delete an existing flashcard in Mochi. To delete set trashed to true.`,
509
- inputSchema: zodToJsonSchema(z.object({
510
- "card-id": z.string(),
511
- ...UpdateCardRequestSchema.shape,
512
- })),
513
- annotations: {
514
- title: "Update flashcard on Mochi",
515
- readOnlyHint: false,
516
- destructiveHint: false,
517
- idempotentHint: false,
518
- openWorldHint: true,
519
- },
520
- },
521
- {
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",
558
- inputSchema: zodToJsonSchema(ListCardsParamsSchema),
559
- annotations: {
560
- title: "List flashcards on Mochi",
561
- readOnlyHint: true,
562
- destructiveHint: false,
563
- idempotentHint: true,
564
- openWorldHint: false,
565
- },
566
- },
567
- {
568
- name: "mochi_list_decks",
569
- description: "List all decks",
570
- inputSchema: zodToJsonSchema(ListDecksParamsSchema),
571
- annotations: {
572
- title: "List decks on Mochi",
573
- readOnlyHint: true,
574
- destructiveHint: false,
575
- idempotentHint: true,
576
- openWorldHint: false,
577
- },
578
- },
579
- {
580
- name: "mochi_list_templates",
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.
584
-
585
- Example response:
586
- {
587
- "bookmark": "g1AAAABAeJzLYWBgYMpgSmHgKy5JLCrJTq2MT8lPzkzJBYpzVBn4JgaaVZiC5Dlg8igyWQAxwRHd",
588
- "docs": [
589
- {
590
- "id": "YDELNZSu",
591
- "name": "Simple flashcard",
592
- "content": "# << Front >>\n---\n<< Back >>",
593
- "pos": "s",
594
- "fields": {
595
- "name": {
596
- "id": "name",
597
- "name": "Front",
598
- "pos": "a"
599
- },
600
- "Ysrde7Lj": {
601
- "id": "Ysrde7Lj",
602
- "name": "Back",
603
- "pos": "m",
604
- "options": {
605
- "multi-line?": true
606
- }
607
- }
608
- }
609
- },
610
- ...
611
- ]
612
- }`,
613
- inputSchema: zodToJsonSchema(ListTemplatesParamsSchema),
614
- annotations: {
615
- title: "List templates on Mochi",
616
- readOnlyHint: true,
617
- destructiveHint: false,
618
- idempotentHint: true,
619
- openWorldHint: false,
620
- },
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
- },
642
- ],
643
- }));
426
+ // Schema for update flashcard tool (combines cardId with update fields)
427
+ const UpdateFlashcardToolSchema = z.object({
428
+ cardId: z.string().describe("ID of the card to update"),
429
+ ...UpdateCardRequestSchema.shape,
430
+ });
431
+ // Output schema for attachment response
432
+ const AddAttachmentResponseSchema = z.object({
433
+ filename: z.string().describe("The filename of the uploaded attachment"),
434
+ markdown: z
435
+ .string()
436
+ .describe("Markdown reference to use in card content, e.g. ![](@media/filename)"),
437
+ });
644
438
  // Create Mochi client
645
439
  const mochiClient = new MochiClient(MOCHI_API_KEY);
646
- // Add resource handlers
647
- server.setRequestHandler(ListResourcesRequestSchema, async () => {
440
+ // Helper to format errors for tool responses
441
+ function formatToolError(error) {
442
+ if (error instanceof z.ZodError) {
443
+ const formattedErrors = error.issues.map((issue) => {
444
+ const path = issue.path.join(".");
445
+ const message = issue.code === "invalid_type" && issue.message.includes("Required")
446
+ ? `Required field '${path}' is missing`
447
+ : issue.message;
448
+ return `${path ? `${path}: ` : ""}${message}`;
449
+ });
450
+ return {
451
+ content: [
452
+ {
453
+ type: "text",
454
+ text: `Validation error:\n${formattedErrors.join("\n")}`,
455
+ },
456
+ ],
457
+ isError: true,
458
+ };
459
+ }
460
+ if (error instanceof MochiError) {
461
+ return {
462
+ content: [
463
+ {
464
+ type: "text",
465
+ text: `Mochi API error (${error.statusCode}): ${error.message}`,
466
+ },
467
+ ],
468
+ isError: true,
469
+ };
470
+ }
648
471
  return {
649
- resources: [
650
- {
651
- uri: `mochi://decks`,
652
- name: "All Mochi Decks",
653
- description: `List of all decks in Mochi.`,
654
- mimeType: "application/json",
655
- },
472
+ content: [
656
473
  {
657
- uri: `mochi://templates`,
658
- name: "All Mochi Templates",
659
- description: `List of all templates in Mochi.`,
660
- mimeType: "application/json",
474
+ type: "text",
475
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`,
661
476
  },
662
477
  ],
478
+ isError: true,
663
479
  };
480
+ }
481
+ // Register tools
482
+ // Note: Using type assertions due to Zod version compatibility between SDK (v4) and project (v3)
483
+ server.registerTool("mochi_create_flashcard", {
484
+ 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.",
486
+ inputSchema: CreateCardRequestSchema,
487
+ outputSchema: CreateCardResponseSchema,
488
+ annotations: {
489
+ readOnlyHint: false,
490
+ destructiveHint: false,
491
+ idempotentHint: false,
492
+ openWorldHint: true,
493
+ },
494
+ }, async (args) => {
495
+ try {
496
+ const response = await mochiClient.createCard(args);
497
+ return {
498
+ content: [{ type: "text", text: JSON.stringify(response, null, 2) }],
499
+ structuredContent: response,
500
+ };
501
+ }
502
+ catch (error) {
503
+ return formatToolError(error);
504
+ }
664
505
  });
665
- server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
666
- const uri = request.params.uri;
667
- switch (uri) {
668
- case "mochi://decks": {
669
- const decks = await mochiClient.listDecks();
670
- return {
671
- contents: [
672
- {
673
- uri,
674
- mimeType: "application/json",
675
- text: JSON.stringify(decks.map((deck) => ({
676
- id: deck.id,
677
- name: deck.name,
678
- archived: deck["archived?"],
679
- })), null, 2),
680
- },
681
- ],
682
- };
683
- }
684
- case "mochi://templates": {
685
- const templates = await mochiClient.listTemplates();
686
- return {
687
- contents: [
688
- {
689
- uri,
690
- mimeType: "application/json",
691
- text: JSON.stringify(templates, null, 2),
692
- },
693
- ],
694
- };
695
- }
696
- default: {
697
- throw new Error("Invalid resource URI");
698
- }
506
+ server.registerTool("mochi_create_card_from_template", {
507
+ 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.",
509
+ inputSchema: CreateCardFromTemplateSchema,
510
+ outputSchema: CreateCardResponseSchema,
511
+ annotations: {
512
+ readOnlyHint: false,
513
+ destructiveHint: false,
514
+ idempotentHint: false,
515
+ openWorldHint: true,
516
+ },
517
+ }, async (args) => {
518
+ try {
519
+ const response = await mochiClient.createCardFromTemplate(args);
520
+ return {
521
+ content: [{ type: "text", text: JSON.stringify(response, null, 2) }],
522
+ structuredContent: response,
523
+ };
524
+ }
525
+ catch (error) {
526
+ return formatToolError(error);
699
527
  }
700
528
  });
701
- const CreateFlashcardPromptSchema = z.object({
702
- input: z
703
- .string()
704
- .describe("The information to base the flashcard on.")
705
- .optional(),
529
+ server.registerTool("mochi_update_flashcard", {
530
+ title: "Update flashcard on Mochi",
531
+ description: "Update or delete an existing flashcard. Set trashed to true to delete.",
532
+ inputSchema: UpdateFlashcardToolSchema,
533
+ outputSchema: UpdateCardResponseSchema,
534
+ annotations: {
535
+ readOnlyHint: false,
536
+ destructiveHint: false,
537
+ idempotentHint: false,
538
+ openWorldHint: true,
539
+ },
540
+ }, async (args) => {
541
+ try {
542
+ const { cardId, ...updateArgs } = args;
543
+ const response = await mochiClient.updateCard(cardId, updateArgs);
544
+ return {
545
+ content: [{ type: "text", text: JSON.stringify(response, null, 2) }],
546
+ structuredContent: response,
547
+ };
548
+ }
549
+ catch (error) {
550
+ return formatToolError(error);
551
+ }
552
+ });
553
+ server.registerTool("mochi_add_attachment", {
554
+ title: "Add attachment to flashcard on Mochi",
555
+ 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.",
556
+ inputSchema: AddAttachmentSchema,
557
+ outputSchema: AddAttachmentResponseSchema,
558
+ annotations: {
559
+ readOnlyHint: false,
560
+ destructiveHint: false,
561
+ idempotentHint: false,
562
+ openWorldHint: true,
563
+ },
564
+ }, async (args) => {
565
+ try {
566
+ const response = await mochiClient.addAttachment(args);
567
+ return {
568
+ content: [{ type: "text", text: JSON.stringify(response, null, 2) }],
569
+ structuredContent: response,
570
+ };
571
+ }
572
+ catch (error) {
573
+ return formatToolError(error);
574
+ }
575
+ });
576
+ server.registerTool("mochi_list_flashcards", {
577
+ title: "List flashcards on Mochi",
578
+ description: "List flashcards, optionally filtered by deck. Returns paginated results.",
579
+ inputSchema: ListCardsParamsSchema.shape,
580
+ outputSchema: ListCardsResponseSchema,
581
+ annotations: {
582
+ readOnlyHint: true,
583
+ destructiveHint: false,
584
+ idempotentHint: true,
585
+ openWorldHint: false,
586
+ },
587
+ }, async (args) => {
588
+ try {
589
+ const response = await mochiClient.listCards(args);
590
+ return {
591
+ content: [{ type: "text", text: JSON.stringify(response, null, 2) }],
592
+ structuredContent: response,
593
+ };
594
+ }
595
+ catch (error) {
596
+ return formatToolError(error);
597
+ }
598
+ });
599
+ server.registerTool("mochi_list_decks", {
600
+ title: "List decks on Mochi",
601
+ description: "List all decks. Use to get deckId for other operations.",
602
+ inputSchema: ListDecksParamsSchema.shape,
603
+ outputSchema: ListDecksResponseSchema,
604
+ annotations: {
605
+ readOnlyHint: true,
606
+ destructiveHint: false,
607
+ idempotentHint: true,
608
+ openWorldHint: false,
609
+ },
610
+ }, async (args) => {
611
+ try {
612
+ const response = await mochiClient.listDecks(args);
613
+ return {
614
+ content: [{ type: "text", text: JSON.stringify(response, null, 2) }],
615
+ structuredContent: { bookmark: "", docs: response },
616
+ };
617
+ }
618
+ catch (error) {
619
+ return formatToolError(error);
620
+ }
706
621
  });
707
- server.setRequestHandler(ListPromptsRequestSchema, async () => {
622
+ server.registerTool("mochi_list_templates", {
623
+ title: "List templates on Mochi",
624
+ description: "List all templates. Use with mochi_create_card_from_template for easy template-based card creation.",
625
+ inputSchema: ListTemplatesParamsSchema.shape,
626
+ outputSchema: ListTemplatesResponseSchema,
627
+ annotations: {
628
+ readOnlyHint: true,
629
+ destructiveHint: false,
630
+ idempotentHint: true,
631
+ openWorldHint: false,
632
+ },
633
+ }, async (args) => {
634
+ try {
635
+ const response = await mochiClient.listTemplates(args);
636
+ return {
637
+ content: [{ type: "text", text: JSON.stringify(response, null, 2) }],
638
+ structuredContent: response,
639
+ };
640
+ }
641
+ catch (error) {
642
+ return formatToolError(error);
643
+ }
644
+ });
645
+ server.registerTool("mochi_get_due_cards", {
646
+ title: "Get due flashcards on Mochi",
647
+ description: "Get flashcards due for review on a specific date (defaults to today).",
648
+ inputSchema: GetDueCardsParamsSchema.shape,
649
+ outputSchema: GetDueCardsResponseSchema,
650
+ annotations: {
651
+ readOnlyHint: true,
652
+ destructiveHint: false,
653
+ idempotentHint: true,
654
+ openWorldHint: false,
655
+ },
656
+ }, async (args) => {
657
+ try {
658
+ const response = await mochiClient.getDueCards(args);
659
+ return {
660
+ content: [{ type: "text", text: JSON.stringify(response, null, 2) }],
661
+ structuredContent: response,
662
+ };
663
+ }
664
+ catch (error) {
665
+ return formatToolError(error);
666
+ }
667
+ });
668
+ // Register resources
669
+ server.registerResource("decks", "mochi://decks", {
670
+ description: "List of all decks in Mochi.",
671
+ mimeType: "application/json",
672
+ }, async () => {
673
+ const decks = await mochiClient.listDecks();
708
674
  return {
709
- prompts: [
675
+ contents: [
710
676
  {
711
- name: "write-flashcard",
712
- description: "Write a flashcard based on user-provided information.",
713
- arguments: [
714
- {
715
- name: "input",
716
- description: "The information to base the flashcard on.",
717
- },
718
- ],
677
+ uri: "mochi://decks",
678
+ mimeType: "application/json",
679
+ text: JSON.stringify(decks.map((deck) => ({ id: deck.id, name: deck.name })), null, 2),
719
680
  },
720
681
  ],
721
682
  };
722
683
  });
723
- server.setRequestHandler(GetPromptRequestSchema, async (request) => {
724
- const params = CreateFlashcardPromptSchema.parse(request.params.arguments);
725
- const { input } = params;
684
+ server.registerResource("templates", "mochi://templates", {
685
+ description: "List of all templates in Mochi.",
686
+ mimeType: "application/json",
687
+ }, async () => {
688
+ const templates = await mochiClient.listTemplates();
726
689
  return {
727
- messages: [
690
+ contents: [
728
691
  {
729
- role: "user",
730
- content: {
731
- type: "text",
732
- text: `Create a flashcard using the info below while adhering to these principles:
692
+ uri: "mochi://templates",
693
+ mimeType: "application/json",
694
+ text: JSON.stringify(templates, null, 2),
695
+ },
696
+ ],
697
+ };
698
+ });
699
+ // Register prompts
700
+ server.registerPrompt("write-flashcard", {
701
+ description: "Write a flashcard based on user-provided information.",
702
+ argsSchema: {
703
+ input: z
704
+ .string()
705
+ .describe("The information to base the flashcard on.")
706
+ .optional(),
707
+ },
708
+ }, async ({ input }) => ({
709
+ messages: [
710
+ {
711
+ role: "user",
712
+ content: {
713
+ type: "text",
714
+ text: `Create a flashcard using the info below while adhering to these principles:
733
715
  - Keep questions and answers atomic.
734
716
  - Utilize cloze prompts when applicable, like "This is a text with {{hidden}} part. Then don't use '---' separator.".
735
717
  - Focus on effective retrieval practice by being concise and clear.
736
718
  - Make it just challenging enough to reinforce specific facts.
737
719
  Input: ${input}
738
720
  `,
739
- },
740
721
  },
741
- ],
742
- };
743
- });
744
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
745
- try {
746
- switch (request.params.name) {
747
- case "mochi_create_flashcard": {
748
- const validatedArgs = CreateCardRequestSchema.parse(request.params.arguments);
749
- const response = await mochiClient.createCard(validatedArgs);
750
- return {
751
- content: [
752
- {
753
- type: "text",
754
- text: JSON.stringify(response, null, 2),
755
- },
756
- ],
757
- isError: false,
758
- };
759
- }
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": {
774
- const { "card-id": cardId, ...updateArgs } = z
775
- .object({
776
- "card-id": z.string(),
777
- ...UpdateCardRequestSchema.shape,
778
- })
779
- .parse(request.params.arguments);
780
- const response = await mochiClient.updateCard(cardId, updateArgs);
781
- return {
782
- content: [
783
- {
784
- type: "text",
785
- text: JSON.stringify(response, null, 2),
786
- },
787
- ],
788
- isError: false,
789
- };
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
- }
804
- case "mochi_list_decks": {
805
- const validatedArgs = ListDecksParamsSchema.parse(request.params.arguments);
806
- const response = await mochiClient.listDecks(validatedArgs);
807
- return {
808
- content: [
809
- {
810
- type: "text",
811
- text: JSON.stringify(response, null, 2),
812
- },
813
- ],
814
- isError: false,
815
- };
816
- }
817
- case "mochi_list_flashcards": {
818
- const validatedArgs = ListCardsParamsSchema.parse(request.params.arguments);
819
- const response = await mochiClient.listCards(validatedArgs);
820
- return {
821
- content: [
822
- {
823
- type: "text",
824
- text: JSON.stringify(response, null, 2),
825
- },
826
- ],
827
- isError: false,
828
- };
829
- }
830
- case "mochi_list_templates": {
831
- const validatedArgs = ListTemplatesParamsSchema.parse(request.params.arguments);
832
- const response = await mochiClient.listTemplates(validatedArgs);
833
- return {
834
- content: [
835
- {
836
- type: "text",
837
- text: JSON.stringify(response, null, 2),
838
- },
839
- ],
840
- isError: false,
841
- };
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
- }
856
- default:
857
- return {
858
- content: [
859
- {
860
- type: "text",
861
- text: `Unknown tool: ${request.params.name}`,
862
- },
863
- ],
864
- isError: true,
865
- };
866
- }
867
- }
868
- catch (error) {
869
- if (error instanceof z.ZodError) {
870
- const formattedErrors = error.errors.map((err) => {
871
- const path = err.path.join(".");
872
- const message = err.code === "invalid_type" && err.message.includes("Required")
873
- ? `Required field '${path}' is missing`
874
- : err.message;
875
- return `${path ? `${path}: ` : ""}${message}`;
876
- });
877
- return {
878
- content: [
879
- {
880
- type: "text",
881
- text: `Validation error:\n${formattedErrors.join("\n")}`,
882
- },
883
- ],
884
- isError: true,
885
- };
886
- }
887
- if (error instanceof MochiError) {
888
- return {
889
- content: [
890
- {
891
- type: "text",
892
- text: `Mochi API error (${error.statusCode}): ${error.message}`,
893
- },
894
- ],
895
- isError: true,
896
- };
897
- }
898
- return {
899
- content: [
900
- {
901
- type: "text",
902
- text: `Error: ${error instanceof Error ? error.message : String(error)}`,
903
- },
904
- ],
905
- isError: true,
906
- };
907
- }
908
- });
722
+ },
723
+ ],
724
+ }));
909
725
  async function runServer() {
910
726
  const transport = new StdioServerTransport();
911
727
  await server.connect(transport);