@fredrika/mcp-mochi 1.0.5 → 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,11 +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
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import FormData from "form-data";
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
6
  import dotenv from "dotenv";
7
7
  import { z } from "zod";
8
- import { zodToJsonSchema } from "zod-to-json-schema";
9
8
  dotenv.config();
10
9
  /**
11
10
  * Custom error class for Mochi API errors
@@ -36,14 +35,14 @@ const CreateCardRequestSchema = z.object({
36
35
  .string()
37
36
  .min(1)
38
37
  .describe("Markdown content of the card. Separate front and back using a horizontal rule (---) or use brackets for {{cloze deletion}}."),
39
- "deck-id": z.string().min(1).describe("ID of the deck to create the card in"),
40
- "template-id": z
38
+ deckId: z.string().min(1).describe("ID of the deck to create the card in"),
39
+ templateId: z
41
40
  .string()
42
41
  .optional()
43
42
  .nullable()
44
43
  .default(null)
45
44
  .describe("Optional template ID to use for the card. Defaults to null if not set."),
46
- "manual-tags": z
45
+ tags: z
47
46
  .array(z.string())
48
47
  .optional()
49
48
  .describe("Optional array of tags to add to the card"),
@@ -57,16 +56,10 @@ const UpdateCardRequestSchema = z.object({
57
56
  .string()
58
57
  .optional()
59
58
  .describe("Updated markdown content of the card"),
60
- "deck-id": z
61
- .string()
62
- .optional()
63
- .describe("ID of the deck to move the card to"),
64
- "template-id": z
65
- .string()
66
- .optional()
67
- .describe("Template ID to use for the card"),
68
- "archived?": z.boolean().optional().describe("Whether the card is archived"),
69
- "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"),
70
63
  fields: z
71
64
  .record(z.string(), CreateCardFieldSchema)
72
65
  .optional()
@@ -79,7 +72,7 @@ const ListDecksParamsSchema = z.object({
79
72
  .describe("Pagination bookmark for fetching next page of results"),
80
73
  });
81
74
  const ListCardsParamsSchema = z.object({
82
- "deck-id": z.string().optional().describe("Get cards from deck ID"),
75
+ deckId: z.string().optional().describe("Get cards from deck ID"),
83
76
  limit: z
84
77
  .number()
85
78
  .min(1)
@@ -97,10 +90,96 @@ const ListTemplatesParamsSchema = z.object({
97
90
  .optional()
98
91
  .describe("Pagination bookmark for fetching next page of results"),
99
92
  });
93
+ const GetDueCardsParamsSchema = z.object({
94
+ deckId: z
95
+ .string()
96
+ .optional()
97
+ .describe("Optional deck ID to filter due cards by a specific deck"),
98
+ date: z
99
+ .string()
100
+ .optional()
101
+ .describe("Optional ISO 8601 date to get cards due on that date. Defaults to today."),
102
+ });
103
+ const CreateCardFromTemplateSchema = z.object({
104
+ templateId: z
105
+ .string()
106
+ .min(1)
107
+ .describe("ID of the template to use. Get this from mochi_list_templates."),
108
+ deckId: z
109
+ .string()
110
+ .min(1)
111
+ .describe("ID of the deck to create the card in. Get this from mochi_list_decks."),
112
+ fields: z
113
+ .record(z.string(), z.string())
114
+ .describe('Map of field NAMES (not IDs) to values. E.g., { "Word": "serendipity" } or { "Front": "Question?", "Back": "Answer" }'),
115
+ tags: z
116
+ .array(z.string())
117
+ .optional()
118
+ .describe("Optional array of tags to add to the card"),
119
+ });
120
+ // Schema for adding attachments
121
+ const AddAttachmentSchema = z.object({
122
+ cardId: z.string().min(1).describe("ID of the card to attach the file to"),
123
+ data: z.string().min(1).describe("Base64-encoded file data"),
124
+ filename: z
125
+ .string()
126
+ .min(1)
127
+ .describe("Filename with extension (e.g., 'image.png', 'audio.mp3')"),
128
+ contentType: z
129
+ .string()
130
+ .optional()
131
+ .describe("MIME type of the file (e.g., 'image/png'). Can be inferred from filename if not provided."),
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
+ }
100
169
  const TemplateFieldSchema = z.object({
101
170
  id: z.string().describe("Unique identifier for the template field"),
102
171
  name: z.string().describe("Display name of the field"),
103
172
  pos: z.string().describe("Position of the field in the template"),
173
+ type: z
174
+ .string()
175
+ .optional()
176
+ .nullable()
177
+ .describe("Field type: null/text for user input, or ai/speech/translate/dictionary for auto-generated"),
178
+ source: z
179
+ .string()
180
+ .optional()
181
+ .nullable()
182
+ .describe("Source field ID for auto-generated fields"),
104
183
  options: z
105
184
  .object({
106
185
  "multi-line?": z
@@ -108,6 +187,7 @@ const TemplateFieldSchema = z.object({
108
187
  .optional()
109
188
  .describe("Whether the field supports multiple lines of text"),
110
189
  })
190
+ .passthrough()
111
191
  .optional()
112
192
  .describe("Additional options for the field"),
113
193
  });
@@ -141,12 +221,13 @@ const CardSchema = z
141
221
  name: z.string().describe("Display name of the card"),
142
222
  "deck-id": z.string().describe("ID of the deck containing the card"),
143
223
  fields: z
144
- .record(z.unknown())
224
+ .record(z.string(), z.unknown())
145
225
  .optional()
146
226
  .describe("Map of field IDs to field values. Need to match the field IDs in the template"),
147
227
  })
148
228
  .strip();
149
229
  const CreateCardResponseSchema = CardSchema.strip();
230
+ const UpdateCardResponseSchema = CardSchema.strip();
150
231
  const ListCardsResponseSchema = z
151
232
  .object({
152
233
  bookmark: z.string().describe("Pagination bookmark for fetching next page"),
@@ -158,7 +239,16 @@ const DeckSchema = z
158
239
  id: z.string().describe("Unique identifier for the deck"),
159
240
  sort: z.number().describe("Sort order of the deck"),
160
241
  name: z.string().describe("Display name of the deck"),
161
- archived: z.boolean().optional().describe("Whether the deck is archived"),
242
+ "archived?": z
243
+ .boolean()
244
+ .optional()
245
+ .nullable()
246
+ .describe("Whether the deck is archived"),
247
+ "trashed?": z
248
+ .object({ date: z.string() })
249
+ .optional()
250
+ .nullable()
251
+ .describe("Whether the deck is trashed"),
162
252
  })
163
253
  .strip();
164
254
  const ListDecksResponseSchema = z
@@ -167,6 +257,18 @@ const ListDecksResponseSchema = z
167
257
  docs: z.array(DeckSchema).describe("Array of decks"),
168
258
  })
169
259
  .strip();
260
+ const DueCardSchema = z
261
+ .object({
262
+ id: z.string().describe("Unique identifier for the card"),
263
+ content: z.string().describe("Markdown content of the card"),
264
+ name: z.string().describe("Display name of the card"),
265
+ "deck-id": z.string().describe("ID of the deck containing the card"),
266
+ "new?": z.boolean().describe("Whether the card is new (never reviewed)"),
267
+ })
268
+ .passthrough();
269
+ const GetDueCardsResponseSchema = z.object({
270
+ cards: z.array(DueCardSchema).describe("Array of cards due for review"),
271
+ });
170
272
  function getApiKey() {
171
273
  const apiKey = process.env.MOCHI_API_KEY;
172
274
  if (!apiKey) {
@@ -190,11 +292,13 @@ export class MochiClient {
190
292
  });
191
293
  }
192
294
  async createCard(request) {
193
- const response = await this.api.post("/cards", request);
295
+ const mochiRequest = toMochiCreateCardRequest(request);
296
+ const response = await this.api.post("/cards", mochiRequest);
194
297
  return CreateCardResponseSchema.parse(response.data);
195
298
  }
196
299
  async updateCard(cardId, request) {
197
- const response = await this.api.post(`/cards/${cardId}`, request);
300
+ const mochiRequest = toMochiUpdateCardRequest(request);
301
+ const response = await this.api.post(`/cards/${cardId}`, mochiRequest);
198
302
  return CreateCardResponseSchema.parse(response.data);
199
303
  }
200
304
  async listDecks(params) {
@@ -202,13 +306,18 @@ export class MochiClient {
202
306
  ? ListDecksParamsSchema.parse(params)
203
307
  : undefined;
204
308
  const response = await this.api.get("/decks", { params: validatedParams });
205
- return ListDecksResponseSchema.parse(response.data).docs.filter((deck) => !deck.archived);
309
+ return ListDecksResponseSchema.parse(response.data)
310
+ .docs.filter((deck) => !deck["archived?"] && !deck["trashed?"])
311
+ .sort((a, b) => a.sort - b.sort);
206
312
  }
207
313
  async listCards(params) {
208
314
  const validatedParams = params
209
315
  ? ListCardsParamsSchema.parse(params)
210
316
  : undefined;
211
- 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 });
212
321
  return ListCardsResponseSchema.parse(response.data);
213
322
  }
214
323
  async listTemplates(params) {
@@ -220,386 +329,399 @@ export class MochiClient {
220
329
  });
221
330
  return ListTemplatesResponseSchema.parse(response.data);
222
331
  }
332
+ async getDueCards(params) {
333
+ const validatedParams = params
334
+ ? GetDueCardsParamsSchema.parse(params)
335
+ : undefined;
336
+ const deckId = validatedParams?.deckId;
337
+ const endpoint = deckId ? `/due/${deckId}` : "/due";
338
+ const queryParams = validatedParams?.date
339
+ ? { date: validatedParams.date }
340
+ : undefined;
341
+ const response = await this.api.get(endpoint, { params: queryParams });
342
+ return GetDueCardsResponseSchema.parse(response.data);
343
+ }
344
+ async getTemplate(templateId) {
345
+ const response = await this.api.get(`/templates/${templateId}`);
346
+ return TemplateSchema.parse(response.data);
347
+ }
348
+ async createCardFromTemplate(request) {
349
+ // Fetch the template to get field definitions
350
+ const template = await this.getTemplate(request.templateId);
351
+ // Map field names to IDs
352
+ const fieldNameToId = {};
353
+ for (const [fieldId, field] of Object.entries(template.fields)) {
354
+ fieldNameToId[field.name] = fieldId;
355
+ }
356
+ // Build the fields object with IDs
357
+ const fields = {};
358
+ const fieldValues = [];
359
+ for (const [fieldName, value] of Object.entries(request.fields)) {
360
+ const fieldId = fieldNameToId[fieldName];
361
+ if (!fieldId) {
362
+ throw new MochiError([
363
+ `Unknown field name: "${fieldName}". Available fields: ${Object.keys(fieldNameToId).join(", ")}`,
364
+ ], 400);
365
+ }
366
+ fields[fieldId] = { id: fieldId, value };
367
+ fieldValues.push(value);
368
+ }
369
+ // Build content from field values (joined with separator for multi-field templates)
370
+ const content = fieldValues.join("\n---\n");
371
+ const createRequest = {
372
+ content,
373
+ deckId: request.deckId,
374
+ templateId: request.templateId,
375
+ fields,
376
+ tags: request.tags,
377
+ };
378
+ return this.createCard(createRequest);
379
+ }
380
+ async addAttachment(request) {
381
+ // Infer content-type from filename if not provided
382
+ let contentType = request.contentType;
383
+ if (!contentType) {
384
+ const ext = request.filename.split(".").pop()?.toLowerCase();
385
+ const mimeTypes = {
386
+ png: "image/png",
387
+ jpg: "image/jpeg",
388
+ jpeg: "image/jpeg",
389
+ gif: "image/gif",
390
+ webp: "image/webp",
391
+ svg: "image/svg+xml",
392
+ mp3: "audio/mpeg",
393
+ wav: "audio/wav",
394
+ ogg: "audio/ogg",
395
+ mp4: "video/mp4",
396
+ pdf: "application/pdf",
397
+ };
398
+ contentType = mimeTypes[ext ?? ""] ?? "application/octet-stream";
399
+ }
400
+ // Convert base64 to Buffer
401
+ const buffer = Buffer.from(request.data, "base64");
402
+ // Create form data
403
+ const formData = new FormData();
404
+ formData.append("file", buffer, {
405
+ filename: request.filename,
406
+ contentType,
407
+ });
408
+ // Upload attachment
409
+ await this.api.post(`/cards/${request.cardId}/attachments/${encodeURIComponent(request.filename)}`, formData, {
410
+ headers: {
411
+ ...formData.getHeaders(),
412
+ Authorization: `Basic ${Buffer.from(`${this.token}:`).toString("base64")}`,
413
+ },
414
+ });
415
+ return {
416
+ filename: request.filename,
417
+ markdown: `![](@media/${request.filename})`,
418
+ };
419
+ }
223
420
  }
224
421
  // Server setup
225
- const server = new Server({
422
+ const server = new McpServer({
226
423
  name: "mcp-server/mochi",
227
424
  version: "1.0.3",
228
- }, {
229
- capabilities: {
230
- tools: {},
231
- resources: {},
232
- prompts: {},
233
- },
234
425
  });
235
- // Set up request handlers
236
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
237
- tools: [
238
- {
239
- name: "mochi_create_card",
240
- 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
-
242
- ## Parameters
243
-
244
- ### deck-id (required)
245
- ALWAYS look up deck-id with the mochi_list_decks tool.
246
-
247
- ### content (required)
248
- The markdown content of the card. Separate front and back using a horizontal rule (---).
249
-
250
- ### template-id (optional)
251
- When using a template, the field ids MUST match the template ones. If not using a template, omit this field.
252
-
253
- ### fields (optional)
254
- 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.
255
-
256
- ## Example without template
257
- {
258
- "content": "What is the capital of France?\n---\nParis",
259
- "deck-id": "btmZUXWM"
260
- }
261
-
262
- ## Example with template
263
- {
264
- "content": "New card from API. ![](@media/foobar03.png)",
265
- "deck-id": "btmZUXWM",
266
- "template-id": "8BtaEAXe",
267
- "fields": {
268
- "name": {
269
- "id": "name",
270
- "value": "Hello,"
271
- },
272
- "JNEnw1e7": {
273
- "id": "JNEnw1e7",
274
- "value": "World!"
275
- }
276
- }
277
- }
278
-
279
- ## Properties of good flashcards:
280
- - **focused:** A question or answer involving too much detail will dull your concentration and stimulate incomplete retrievals, leaving some bulbs unlit.
281
- - **precise** about what they're asking for. Vague questions will elicit vague answers, which won't reliably light the bulbs you're targeting.
282
- - **consistent** answers, lighting the same bulbs each time you perform the task.
283
- - **tractable**: Write prompts which you can almost always answer correctly. This often means breaking the task down, or adding cues
284
- - **effortful**: You shouldn't be able to trivially infer the answer.
285
- `,
286
- inputSchema: zodToJsonSchema(CreateCardRequestSchema),
287
- annotations: {
288
- title: "Create flashcard on Mochi",
289
- readOnlyHint: false,
290
- destructiveHint: false,
291
- idempotentHint: false,
292
- openWorldHint: true,
293
- },
294
- },
295
- {
296
- name: "mochi_update_card",
297
- description: `Update or delete an existing flashcard in Mochi. To delete set trashed to true.`,
298
- inputSchema: zodToJsonSchema(z.object({
299
- "card-id": z.string(),
300
- ...UpdateCardRequestSchema.shape,
301
- })),
302
- annotations: {
303
- title: "Update flashcard on Mochi",
304
- readOnlyHint: false,
305
- destructiveHint: false,
306
- idempotentHint: false,
307
- openWorldHint: true,
308
- },
309
- },
310
- {
311
- name: "mochi_list_cards",
312
- description: "List cards in pages of 10 cards per page",
313
- inputSchema: zodToJsonSchema(ListCardsParamsSchema),
314
- annotations: {
315
- title: "List flashcards on Mochi",
316
- readOnlyHint: true,
317
- destructiveHint: false,
318
- idempotentHint: true,
319
- openWorldHint: false,
320
- },
321
- },
322
- {
323
- name: "mochi_list_decks",
324
- description: "List all decks",
325
- inputSchema: zodToJsonSchema(ListDecksParamsSchema),
326
- annotations: {
327
- title: "List decks on Mochi",
328
- readOnlyHint: true,
329
- destructiveHint: false,
330
- idempotentHint: true,
331
- openWorldHint: false,
332
- },
333
- },
334
- {
335
- name: "mochi_list_templates",
336
- description: `Templates can be used to create cards with pre-defined fields using the template_id field.
337
-
338
- Example response:
339
- {
340
- "bookmark": "g1AAAABAeJzLYWBgYMpgSmHgKy5JLCrJTq2MT8lPzkzJBYpzVBn4JgaaVZiC5Dlg8igyWQAxwRHd",
341
- "docs": [
342
- {
343
- "id": "YDELNZSu",
344
- "name": "Simple flashcard",
345
- "content": "# << Front >>\n---\n<< Back >>",
346
- "pos": "s",
347
- "fields": {
348
- "name": {
349
- "id": "name",
350
- "name": "Front",
351
- "pos": "a"
352
- },
353
- "Ysrde7Lj": {
354
- "id": "Ysrde7Lj",
355
- "name": "Back",
356
- "pos": "m",
357
- "options": {
358
- "multi-line?": true
359
- }
360
- }
361
- }
362
- },
363
- ...
364
- ]
365
- }`,
366
- inputSchema: zodToJsonSchema(ListTemplatesParamsSchema),
367
- annotations: {
368
- title: "List templates on Mochi",
369
- readOnlyHint: true,
370
- destructiveHint: false,
371
- idempotentHint: true,
372
- openWorldHint: false,
373
- },
374
- },
375
- ],
376
- }));
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
+ });
377
438
  // Create Mochi client
378
439
  const mochiClient = new MochiClient(MOCHI_API_KEY);
379
- // Add resource handlers
380
- 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
+ }
381
471
  return {
382
- resources: [
472
+ content: [
383
473
  {
384
- uri: `mochi://decks`,
385
- name: "All Mochi Decks",
386
- description: `List of all decks in Mochi.`,
387
- mimeType: "application/json",
388
- },
389
- {
390
- uri: `mochi://templates`,
391
- name: "All Mochi Templates",
392
- description: `List of all templates in Mochi.`,
393
- mimeType: "application/json",
474
+ type: "text",
475
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`,
394
476
  },
395
477
  ],
478
+ isError: true,
396
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
+ }
397
505
  });
398
- server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
399
- const uri = request.params.uri;
400
- switch (uri) {
401
- case "mochi://decks": {
402
- const decks = await mochiClient.listDecks();
403
- return {
404
- contents: [
405
- {
406
- uri,
407
- mimeType: "application/json",
408
- text: JSON.stringify(decks.map((deck) => ({
409
- id: deck.id,
410
- name: deck.name,
411
- archived: deck.archived,
412
- })), null, 2),
413
- },
414
- ],
415
- };
416
- }
417
- case "mochi://templates": {
418
- const templates = await mochiClient.listTemplates();
419
- return {
420
- contents: [
421
- {
422
- uri,
423
- mimeType: "application/json",
424
- text: JSON.stringify(templates, null, 2),
425
- },
426
- ],
427
- };
428
- }
429
- default: {
430
- throw new Error("Invalid resource URI");
431
- }
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);
432
527
  }
433
528
  });
434
- const CreateFlashcardPromptSchema = z.object({
435
- input: z
436
- .string()
437
- .describe("The information to base the flashcard on.")
438
- .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
+ }
621
+ });
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
+ }
439
667
  });
440
- server.setRequestHandler(ListPromptsRequestSchema, async () => {
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();
441
674
  return {
442
- prompts: [
675
+ contents: [
443
676
  {
444
- name: "write-flashcard",
445
- description: "Write a flashcard based on user-provided information.",
446
- arguments: [
447
- {
448
- name: "input",
449
- description: "The information to base the flashcard on.",
450
- },
451
- ],
677
+ uri: "mochi://decks",
678
+ mimeType: "application/json",
679
+ text: JSON.stringify(decks.map((deck) => ({ id: deck.id, name: deck.name })), null, 2),
452
680
  },
453
681
  ],
454
682
  };
455
683
  });
456
- server.setRequestHandler(GetPromptRequestSchema, async (request) => {
457
- const params = CreateFlashcardPromptSchema.parse(request.params.arguments);
458
- 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();
459
689
  return {
460
- messages: [
690
+ contents: [
461
691
  {
462
- role: "user",
463
- content: {
464
- type: "text",
465
- 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:
466
715
  - Keep questions and answers atomic.
467
716
  - Utilize cloze prompts when applicable, like "This is a text with {{hidden}} part. Then don't use '---' separator.".
468
717
  - Focus on effective retrieval practice by being concise and clear.
469
718
  - Make it just challenging enough to reinforce specific facts.
470
719
  Input: ${input}
471
720
  `,
472
- },
473
721
  },
474
- ],
475
- };
476
- });
477
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
478
- try {
479
- switch (request.params.name) {
480
- case "mochi_create_card": {
481
- const validatedArgs = CreateCardRequestSchema.parse(request.params.arguments);
482
- const response = await mochiClient.createCard(validatedArgs);
483
- return {
484
- content: [
485
- {
486
- type: "text",
487
- text: JSON.stringify(response, null, 2),
488
- },
489
- ],
490
- isError: false,
491
- };
492
- }
493
- case "mochi_update_card": {
494
- const { "card-id": cardId, ...updateArgs } = z
495
- .object({
496
- "card-id": z.string(),
497
- ...UpdateCardRequestSchema.shape,
498
- })
499
- .parse(request.params.arguments);
500
- const response = await mochiClient.updateCard(cardId, updateArgs);
501
- return {
502
- content: [
503
- {
504
- type: "text",
505
- text: JSON.stringify(response, null, 2),
506
- },
507
- ],
508
- isError: false,
509
- };
510
- }
511
- case "mochi_list_decks": {
512
- const validatedArgs = ListDecksParamsSchema.parse(request.params.arguments);
513
- const response = await mochiClient.listDecks(validatedArgs);
514
- return {
515
- content: [
516
- {
517
- type: "text",
518
- text: JSON.stringify(response, null, 2),
519
- },
520
- ],
521
- isError: false,
522
- };
523
- }
524
- case "mochi_list_cards": {
525
- const validatedArgs = ListCardsParamsSchema.parse(request.params.arguments);
526
- const response = await mochiClient.listCards(validatedArgs);
527
- return {
528
- content: [
529
- {
530
- type: "text",
531
- text: JSON.stringify(response, null, 2),
532
- },
533
- ],
534
- isError: false,
535
- };
536
- }
537
- case "mochi_list_templates": {
538
- const validatedArgs = ListTemplatesParamsSchema.parse(request.params.arguments);
539
- const response = await mochiClient.listTemplates(validatedArgs);
540
- return {
541
- content: [
542
- {
543
- type: "text",
544
- text: JSON.stringify(response, null, 2),
545
- },
546
- ],
547
- isError: false,
548
- };
549
- }
550
- default:
551
- return {
552
- content: [
553
- {
554
- type: "text",
555
- text: `Unknown tool: ${request.params.name}`,
556
- },
557
- ],
558
- isError: true,
559
- };
560
- }
561
- }
562
- catch (error) {
563
- if (error instanceof z.ZodError) {
564
- const formattedErrors = error.errors.map((err) => {
565
- const path = err.path.join(".");
566
- const message = err.code === "invalid_type" && err.message.includes("Required")
567
- ? `Required field '${path}' is missing`
568
- : err.message;
569
- return `${path ? `${path}: ` : ""}${message}`;
570
- });
571
- return {
572
- content: [
573
- {
574
- type: "text",
575
- text: `Validation error:\n${formattedErrors.join("\n")}`,
576
- },
577
- ],
578
- isError: true,
579
- };
580
- }
581
- if (error instanceof MochiError) {
582
- return {
583
- content: [
584
- {
585
- type: "text",
586
- text: `Mochi API error (${error.statusCode}): ${error.message}`,
587
- },
588
- ],
589
- isError: true,
590
- };
591
- }
592
- return {
593
- content: [
594
- {
595
- type: "text",
596
- text: `Error: ${error instanceof Error ? error.message : String(error)}`,
597
- },
598
- ],
599
- isError: true,
600
- };
601
- }
602
- });
722
+ },
723
+ ],
724
+ }));
603
725
  async function runServer() {
604
726
  const transport = new StdioServerTransport();
605
727
  await server.connect(transport);