@bojanrajkovic/mcp-paprika 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +42 -0
  3. package/dist/cache/disk-cache.d.ts +21 -0
  4. package/dist/cache/disk-cache.js +252 -0
  5. package/dist/cache/recipe-store.d.ts +33 -0
  6. package/dist/cache/recipe-store.js +189 -0
  7. package/dist/features/discover-feature.d.ts +5 -0
  8. package/dist/features/discover-feature.js +39 -0
  9. package/dist/features/embedding-errors.d.ts +26 -0
  10. package/dist/features/embedding-errors.js +34 -0
  11. package/dist/features/embeddings.d.ts +70 -0
  12. package/dist/features/embeddings.js +186 -0
  13. package/dist/features/vector-store-errors.d.ts +12 -0
  14. package/dist/features/vector-store-errors.js +15 -0
  15. package/dist/features/vector-store.d.ts +63 -0
  16. package/dist/features/vector-store.js +202 -0
  17. package/dist/index.d.ts +2 -0
  18. package/dist/index.js +100 -0
  19. package/dist/paprika/client.d.ts +27 -0
  20. package/dist/paprika/client.js +183 -0
  21. package/dist/paprika/errors.d.ts +37 -0
  22. package/dist/paprika/errors.js +48 -0
  23. package/dist/paprika/sync.d.ts +27 -0
  24. package/dist/paprika/sync.js +150 -0
  25. package/dist/paprika/types.d.ts +324 -0
  26. package/dist/paprika/types.js +116 -0
  27. package/dist/resources/recipes.d.ts +3 -0
  28. package/dist/resources/recipes.js +34 -0
  29. package/dist/tools/categories.d.ts +3 -0
  30. package/dist/tools/categories.js +38 -0
  31. package/dist/tools/create.d.ts +3 -0
  32. package/dist/tools/create.js +79 -0
  33. package/dist/tools/delete.d.ts +3 -0
  34. package/dist/tools/delete.js +33 -0
  35. package/dist/tools/discover.d.ts +4 -0
  36. package/dist/tools/discover.js +60 -0
  37. package/dist/tools/filter.d.ts +3 -0
  38. package/dist/tools/filter.js +101 -0
  39. package/dist/tools/helpers.d.ts +31 -0
  40. package/dist/tools/helpers.js +112 -0
  41. package/dist/tools/list.d.ts +3 -0
  42. package/dist/tools/list.js +34 -0
  43. package/dist/tools/read.d.ts +3 -0
  44. package/dist/tools/read.js +42 -0
  45. package/dist/tools/search.d.ts +3 -0
  46. package/dist/tools/search.js +46 -0
  47. package/dist/tools/update.d.ts +3 -0
  48. package/dist/tools/update.js +77 -0
  49. package/dist/types/server-context.d.ts +10 -0
  50. package/dist/types/server-context.js +1 -0
  51. package/dist/utils/config.d.ts +115 -0
  52. package/dist/utils/config.js +197 -0
  53. package/dist/utils/duration.d.ts +10 -0
  54. package/dist/utils/duration.js +86 -0
  55. package/dist/utils/xdg.d.ts +5 -0
  56. package/dist/utils/xdg.js +17 -0
  57. package/package.json +64 -0
@@ -0,0 +1,324 @@
1
+ import { z } from "zod";
2
+ import type { SetRequired } from "type-fest";
3
+ export declare const RecipeUidSchema: z.ZodBranded<z.ZodString, "RecipeUid">;
4
+ export declare const CategoryUidSchema: z.ZodBranded<z.ZodString, "CategoryUid">;
5
+ export type RecipeUid = z.infer<typeof RecipeUidSchema>;
6
+ export type CategoryUid = z.infer<typeof CategoryUidSchema>;
7
+ export declare const RecipeEntrySchema: z.ZodObject<{
8
+ uid: z.ZodBranded<z.ZodString, "RecipeUid">;
9
+ hash: z.ZodString;
10
+ }, "strip", z.ZodTypeAny, {
11
+ uid: string & z.BRAND<"RecipeUid">;
12
+ hash: string;
13
+ }, {
14
+ uid: string;
15
+ hash: string;
16
+ }>;
17
+ export type RecipeEntry = z.infer<typeof RecipeEntrySchema>;
18
+ export declare const RecipeStoredSchema: z.ZodObject<{
19
+ uid: z.ZodBranded<z.ZodString, "RecipeUid">;
20
+ hash: z.ZodString;
21
+ name: z.ZodString;
22
+ categories: z.ZodArray<z.ZodBranded<z.ZodString, "CategoryUid">, "many">;
23
+ ingredients: z.ZodString;
24
+ directions: z.ZodString;
25
+ description: z.ZodNullable<z.ZodString>;
26
+ notes: z.ZodNullable<z.ZodString>;
27
+ prepTime: z.ZodNullable<z.ZodString>;
28
+ cookTime: z.ZodNullable<z.ZodString>;
29
+ totalTime: z.ZodNullable<z.ZodString>;
30
+ servings: z.ZodNullable<z.ZodString>;
31
+ difficulty: z.ZodNullable<z.ZodString>;
32
+ rating: z.ZodNumber;
33
+ created: z.ZodString;
34
+ imageUrl: z.ZodNullable<z.ZodString>;
35
+ photo: z.ZodNullable<z.ZodString>;
36
+ photoHash: z.ZodNullable<z.ZodString>;
37
+ photoLarge: z.ZodNullable<z.ZodString>;
38
+ photoUrl: z.ZodNullable<z.ZodString>;
39
+ source: z.ZodNullable<z.ZodString>;
40
+ sourceUrl: z.ZodNullable<z.ZodString>;
41
+ onFavorites: z.ZodBoolean;
42
+ inTrash: z.ZodBoolean;
43
+ isPinned: z.ZodBoolean;
44
+ onGroceryList: z.ZodBoolean;
45
+ scale: z.ZodNullable<z.ZodString>;
46
+ nutritionalInfo: z.ZodNullable<z.ZodString>;
47
+ }, "strip", z.ZodTypeAny, {
48
+ uid: string & z.BRAND<"RecipeUid">;
49
+ hash: string;
50
+ name: string;
51
+ categories: (string & z.BRAND<"CategoryUid">)[];
52
+ ingredients: string;
53
+ directions: string;
54
+ description: string | null;
55
+ notes: string | null;
56
+ prepTime: string | null;
57
+ cookTime: string | null;
58
+ totalTime: string | null;
59
+ servings: string | null;
60
+ difficulty: string | null;
61
+ rating: number;
62
+ created: string;
63
+ imageUrl: string | null;
64
+ photo: string | null;
65
+ photoHash: string | null;
66
+ photoLarge: string | null;
67
+ photoUrl: string | null;
68
+ source: string | null;
69
+ sourceUrl: string | null;
70
+ onFavorites: boolean;
71
+ inTrash: boolean;
72
+ isPinned: boolean;
73
+ onGroceryList: boolean;
74
+ scale: string | null;
75
+ nutritionalInfo: string | null;
76
+ }, {
77
+ uid: string;
78
+ hash: string;
79
+ name: string;
80
+ categories: string[];
81
+ ingredients: string;
82
+ directions: string;
83
+ description: string | null;
84
+ notes: string | null;
85
+ prepTime: string | null;
86
+ cookTime: string | null;
87
+ totalTime: string | null;
88
+ servings: string | null;
89
+ difficulty: string | null;
90
+ rating: number;
91
+ created: string;
92
+ imageUrl: string | null;
93
+ photo: string | null;
94
+ photoHash: string | null;
95
+ photoLarge: string | null;
96
+ photoUrl: string | null;
97
+ source: string | null;
98
+ sourceUrl: string | null;
99
+ onFavorites: boolean;
100
+ inTrash: boolean;
101
+ isPinned: boolean;
102
+ onGroceryList: boolean;
103
+ scale: string | null;
104
+ nutritionalInfo: string | null;
105
+ }>;
106
+ export type Recipe = z.infer<typeof RecipeStoredSchema>;
107
+ export declare const RecipeSchema: z.ZodEffects<z.ZodObject<{
108
+ uid: z.ZodBranded<z.ZodString, "RecipeUid">;
109
+ hash: z.ZodString;
110
+ name: z.ZodString;
111
+ categories: z.ZodArray<z.ZodBranded<z.ZodString, "CategoryUid">, "many">;
112
+ ingredients: z.ZodString;
113
+ directions: z.ZodString;
114
+ description: z.ZodNullable<z.ZodString>;
115
+ notes: z.ZodNullable<z.ZodString>;
116
+ prep_time: z.ZodNullable<z.ZodString>;
117
+ cook_time: z.ZodNullable<z.ZodString>;
118
+ total_time: z.ZodNullable<z.ZodString>;
119
+ servings: z.ZodNullable<z.ZodString>;
120
+ difficulty: z.ZodNullable<z.ZodString>;
121
+ rating: z.ZodNumber;
122
+ created: z.ZodString;
123
+ image_url: z.ZodNullable<z.ZodString>;
124
+ photo: z.ZodNullable<z.ZodString>;
125
+ photo_hash: z.ZodNullable<z.ZodString>;
126
+ photo_large: z.ZodNullable<z.ZodString>;
127
+ photo_url: z.ZodNullable<z.ZodString>;
128
+ source: z.ZodNullable<z.ZodString>;
129
+ source_url: z.ZodNullable<z.ZodString>;
130
+ on_favorites: z.ZodBoolean;
131
+ in_trash: z.ZodBoolean;
132
+ is_pinned: z.ZodBoolean;
133
+ on_grocery_list: z.ZodBoolean;
134
+ scale: z.ZodNullable<z.ZodString>;
135
+ nutritional_info: z.ZodNullable<z.ZodString>;
136
+ }, "strip", z.ZodTypeAny, {
137
+ uid: string & z.BRAND<"RecipeUid">;
138
+ hash: string;
139
+ name: string;
140
+ categories: (string & z.BRAND<"CategoryUid">)[];
141
+ ingredients: string;
142
+ directions: string;
143
+ description: string | null;
144
+ notes: string | null;
145
+ servings: string | null;
146
+ difficulty: string | null;
147
+ rating: number;
148
+ created: string;
149
+ photo: string | null;
150
+ source: string | null;
151
+ scale: string | null;
152
+ prep_time: string | null;
153
+ cook_time: string | null;
154
+ total_time: string | null;
155
+ image_url: string | null;
156
+ photo_hash: string | null;
157
+ photo_large: string | null;
158
+ photo_url: string | null;
159
+ source_url: string | null;
160
+ on_favorites: boolean;
161
+ in_trash: boolean;
162
+ is_pinned: boolean;
163
+ on_grocery_list: boolean;
164
+ nutritional_info: string | null;
165
+ }, {
166
+ uid: string;
167
+ hash: string;
168
+ name: string;
169
+ categories: string[];
170
+ ingredients: string;
171
+ directions: string;
172
+ description: string | null;
173
+ notes: string | null;
174
+ servings: string | null;
175
+ difficulty: string | null;
176
+ rating: number;
177
+ created: string;
178
+ photo: string | null;
179
+ source: string | null;
180
+ scale: string | null;
181
+ prep_time: string | null;
182
+ cook_time: string | null;
183
+ total_time: string | null;
184
+ image_url: string | null;
185
+ photo_hash: string | null;
186
+ photo_large: string | null;
187
+ photo_url: string | null;
188
+ source_url: string | null;
189
+ on_favorites: boolean;
190
+ in_trash: boolean;
191
+ is_pinned: boolean;
192
+ on_grocery_list: boolean;
193
+ nutritional_info: string | null;
194
+ }>, {
195
+ uid: string & z.BRAND<"RecipeUid">;
196
+ hash: string;
197
+ name: string;
198
+ categories: (string & z.BRAND<"CategoryUid">)[];
199
+ ingredients: string;
200
+ directions: string;
201
+ description: string | null;
202
+ notes: string | null;
203
+ prepTime: string | null;
204
+ cookTime: string | null;
205
+ totalTime: string | null;
206
+ servings: string | null;
207
+ difficulty: string | null;
208
+ rating: number;
209
+ created: string;
210
+ imageUrl: string | null;
211
+ photo: string | null;
212
+ photoHash: string | null;
213
+ photoLarge: string | null;
214
+ photoUrl: string | null;
215
+ source: string | null;
216
+ sourceUrl: string | null;
217
+ onFavorites: boolean;
218
+ inTrash: boolean;
219
+ isPinned: boolean;
220
+ onGroceryList: boolean;
221
+ scale: string | null;
222
+ nutritionalInfo: string | null;
223
+ }, {
224
+ uid: string;
225
+ hash: string;
226
+ name: string;
227
+ categories: string[];
228
+ ingredients: string;
229
+ directions: string;
230
+ description: string | null;
231
+ notes: string | null;
232
+ servings: string | null;
233
+ difficulty: string | null;
234
+ rating: number;
235
+ created: string;
236
+ photo: string | null;
237
+ source: string | null;
238
+ scale: string | null;
239
+ prep_time: string | null;
240
+ cook_time: string | null;
241
+ total_time: string | null;
242
+ image_url: string | null;
243
+ photo_hash: string | null;
244
+ photo_large: string | null;
245
+ photo_url: string | null;
246
+ source_url: string | null;
247
+ on_favorites: boolean;
248
+ in_trash: boolean;
249
+ is_pinned: boolean;
250
+ on_grocery_list: boolean;
251
+ nutritional_info: string | null;
252
+ }>;
253
+ export declare const CategoryStoredSchema: z.ZodObject<{
254
+ uid: z.ZodBranded<z.ZodString, "CategoryUid">;
255
+ name: z.ZodString;
256
+ orderFlag: z.ZodNumber;
257
+ parentUid: z.ZodNullable<z.ZodString>;
258
+ }, "strip", z.ZodTypeAny, {
259
+ uid: string & z.BRAND<"CategoryUid">;
260
+ name: string;
261
+ orderFlag: number;
262
+ parentUid: string | null;
263
+ }, {
264
+ uid: string;
265
+ name: string;
266
+ orderFlag: number;
267
+ parentUid: string | null;
268
+ }>;
269
+ export type Category = z.infer<typeof CategoryStoredSchema>;
270
+ export declare const CategorySchema: z.ZodEffects<z.ZodObject<{
271
+ uid: z.ZodBranded<z.ZodString, "CategoryUid">;
272
+ name: z.ZodString;
273
+ order_flag: z.ZodNumber;
274
+ parent_uid: z.ZodNullable<z.ZodString>;
275
+ }, "strip", z.ZodTypeAny, {
276
+ uid: string & z.BRAND<"CategoryUid">;
277
+ name: string;
278
+ order_flag: number;
279
+ parent_uid: string | null;
280
+ }, {
281
+ uid: string;
282
+ name: string;
283
+ order_flag: number;
284
+ parent_uid: string | null;
285
+ }>, {
286
+ uid: string & z.BRAND<"CategoryUid">;
287
+ name: string;
288
+ orderFlag: number;
289
+ parentUid: string | null;
290
+ }, {
291
+ uid: string;
292
+ name: string;
293
+ order_flag: number;
294
+ parent_uid: string | null;
295
+ }>;
296
+ export declare const AuthResponseSchema: z.ZodObject<{
297
+ result: z.ZodObject<{
298
+ token: z.ZodString;
299
+ }, "strip", z.ZodTypeAny, {
300
+ token: string;
301
+ }, {
302
+ token: string;
303
+ }>;
304
+ }, "strip", z.ZodTypeAny, {
305
+ result: {
306
+ token: string;
307
+ };
308
+ }, {
309
+ result: {
310
+ token: string;
311
+ };
312
+ }>;
313
+ export type AuthResponse = z.output<typeof AuthResponseSchema>;
314
+ export type RecipeInput = SetRequired<Partial<Omit<Recipe, "uid" | "hash" | "created">>, "name" | "ingredients" | "directions">;
315
+ export type SyncResult = {
316
+ readonly added: ReadonlyArray<Recipe>;
317
+ readonly updated: ReadonlyArray<Recipe>;
318
+ readonly removedUids: ReadonlyArray<string>;
319
+ };
320
+ export type DiffResult = {
321
+ readonly added: ReadonlyArray<string>;
322
+ readonly changed: ReadonlyArray<string>;
323
+ readonly removed: ReadonlyArray<string>;
324
+ };
@@ -0,0 +1,116 @@
1
+ import { z } from "zod";
2
+ // Branded UID schemas using z.string().brand()
3
+ export const RecipeUidSchema = z.string().brand("RecipeUid");
4
+ export const CategoryUidSchema = z.string().brand("CategoryUid");
5
+ // Entry schemas for sync list endpoints
6
+ export const RecipeEntrySchema = z.object({
7
+ uid: RecipeUidSchema,
8
+ hash: z.string(),
9
+ });
10
+ // StoredSchema — validates camelCase JSON read back from disk. No transform.
11
+ export const RecipeStoredSchema = z.object({
12
+ uid: RecipeUidSchema,
13
+ hash: z.string(),
14
+ name: z.string(),
15
+ categories: z.array(CategoryUidSchema),
16
+ ingredients: z.string(),
17
+ directions: z.string(),
18
+ description: z.string().nullable(),
19
+ notes: z.string().nullable(),
20
+ prepTime: z.string().nullable(),
21
+ cookTime: z.string().nullable(),
22
+ totalTime: z.string().nullable(),
23
+ servings: z.string().nullable(),
24
+ difficulty: z.string().nullable(),
25
+ rating: z.number().int(),
26
+ created: z.string(),
27
+ imageUrl: z.string().nullable(),
28
+ photo: z.string().nullable(),
29
+ photoHash: z.string().nullable(),
30
+ photoLarge: z.string().nullable(),
31
+ photoUrl: z.string().nullable(),
32
+ source: z.string().nullable(),
33
+ sourceUrl: z.string().nullable(),
34
+ onFavorites: z.boolean(),
35
+ inTrash: z.boolean(),
36
+ isPinned: z.boolean(),
37
+ onGroceryList: z.boolean(),
38
+ scale: z.string().nullable(),
39
+ nutritionalInfo: z.string().nullable(),
40
+ });
41
+ // RecipeSchema — accepts snake_case wire format, transforms to camelCase Recipe.
42
+ // The `: Recipe` annotation on the transform return ensures the compiler enforces
43
+ // that RecipeSchema's output is always structurally identical to RecipeStoredSchema.
44
+ export const RecipeSchema = z
45
+ .object({
46
+ uid: RecipeUidSchema,
47
+ hash: z.string(),
48
+ name: z.string(),
49
+ categories: z.array(CategoryUidSchema),
50
+ ingredients: z.string(),
51
+ directions: z.string(),
52
+ description: z.string().nullable(),
53
+ notes: z.string().nullable(),
54
+ prep_time: z.string().nullable(),
55
+ cook_time: z.string().nullable(),
56
+ total_time: z.string().nullable(),
57
+ servings: z.string().nullable(),
58
+ difficulty: z.string().nullable(),
59
+ rating: z.number().int(),
60
+ created: z.string(),
61
+ image_url: z.string().nullable(),
62
+ photo: z.string().nullable(),
63
+ photo_hash: z.string().nullable(),
64
+ photo_large: z.string().nullable(),
65
+ photo_url: z.string().nullable(),
66
+ source: z.string().nullable(),
67
+ source_url: z.string().nullable(),
68
+ on_favorites: z.boolean(),
69
+ in_trash: z.boolean(),
70
+ is_pinned: z.boolean(),
71
+ on_grocery_list: z.boolean(),
72
+ scale: z.string().nullable(),
73
+ nutritional_info: z.string().nullable(),
74
+ })
75
+ .transform(({ image_url, prep_time, cook_time, total_time, photo_hash, photo_large, photo_url, source_url, on_favorites, in_trash, is_pinned, on_grocery_list, nutritional_info, ...rest }) => ({
76
+ ...rest,
77
+ imageUrl: image_url,
78
+ prepTime: prep_time,
79
+ cookTime: cook_time,
80
+ totalTime: total_time,
81
+ photoHash: photo_hash,
82
+ photoLarge: photo_large,
83
+ photoUrl: photo_url,
84
+ sourceUrl: source_url,
85
+ onFavorites: on_favorites,
86
+ inTrash: in_trash,
87
+ isPinned: is_pinned,
88
+ onGroceryList: on_grocery_list,
89
+ nutritionalInfo: nutritional_info,
90
+ }));
91
+ // StoredSchema — validates camelCase JSON read back from disk. No transform.
92
+ export const CategoryStoredSchema = z.object({
93
+ uid: CategoryUidSchema,
94
+ name: z.string(),
95
+ orderFlag: z.number().int(),
96
+ parentUid: z.string().nullable(),
97
+ });
98
+ // CategorySchema — accepts snake_case wire format, transforms to camelCase Category.
99
+ export const CategorySchema = z
100
+ .object({
101
+ uid: CategoryUidSchema,
102
+ name: z.string(),
103
+ order_flag: z.number().int(),
104
+ parent_uid: z.string().nullable(),
105
+ })
106
+ .transform(({ order_flag, parent_uid, ...rest }) => ({
107
+ ...rest,
108
+ orderFlag: order_flag,
109
+ parentUid: parent_uid,
110
+ }));
111
+ // AuthResponseSchema - nested object, no transform needed
112
+ export const AuthResponseSchema = z.object({
113
+ result: z.object({
114
+ token: z.string(),
115
+ }),
116
+ });
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { ServerContext } from "../types/server-context.js";
3
+ export declare function registerRecipeResources(server: McpServer, ctx: ServerContext): void;
@@ -0,0 +1,34 @@
1
+ import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { recipeToMarkdown } from "../tools/helpers.js";
3
+ export function registerRecipeResources(server, ctx) {
4
+ const template = new ResourceTemplate("paprika://recipe/{uid}", {
5
+ list: async () => {
6
+ const recipes = ctx.store.getAll();
7
+ return {
8
+ resources: recipes.map((recipe) => ({
9
+ uri: `paprika://recipe/${recipe.uid}`,
10
+ name: recipe.name,
11
+ mimeType: "text/markdown",
12
+ })),
13
+ };
14
+ },
15
+ });
16
+ server.registerResource("recipes", template, { description: "Paprika recipes accessible by UID" }, async (uri, variables) => {
17
+ const uid = variables["uid"];
18
+ const recipe = ctx.store.get(uid);
19
+ if (!recipe) {
20
+ throw new Error(`Recipe not found: ${uid}`);
21
+ }
22
+ const categoryNames = ctx.store.resolveCategories(recipe.categories);
23
+ const content = `**UID:** \`${uid}\`\n\n${recipeToMarkdown(recipe, categoryNames)}`;
24
+ return {
25
+ contents: [
26
+ {
27
+ uri: uri.href,
28
+ mimeType: "text/markdown",
29
+ text: content,
30
+ },
31
+ ],
32
+ };
33
+ });
34
+ }
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { ServerContext } from "../types/server-context.js";
3
+ export declare function registerCategoryTools(server: McpServer, ctx: ServerContext): void;
@@ -0,0 +1,38 @@
1
+ import { coldStartGuard, textResult } from "./helpers.js";
2
+ export function registerCategoryTools(server, ctx) {
3
+ server.registerTool("list_categories", {
4
+ description: "List all recipe categories with the number of recipes in each. Categories are sorted alphabetically.",
5
+ inputSchema: {},
6
+ }, async (_args) => {
7
+ return coldStartGuard(ctx).match(async () => {
8
+ const categories = ctx.store.getAllCategories();
9
+ if (categories.length === 0) {
10
+ return textResult("No categories found in your recipe library.");
11
+ }
12
+ const recipes = ctx.store.getAll();
13
+ // Initialize every category with count 0 so categories with no recipes
14
+ // still appear in the output (AC4.3).
15
+ const countMap = new Map();
16
+ for (const category of categories) {
17
+ countMap.set(category.uid, 0);
18
+ }
19
+ // Increment count for each non-trashed recipe's categories.
20
+ // getAll() already excludes trashed recipes.
21
+ for (const recipe of recipes) {
22
+ for (const uid of recipe.categories) {
23
+ const current = countMap.get(uid) ?? 0;
24
+ countMap.set(uid, current + 1);
25
+ }
26
+ }
27
+ const sorted = categories.toSorted((a, b) => a.name.localeCompare(b.name));
28
+ return textResult(formatCategoryList(sorted, countMap));
29
+ }, (guard) => guard);
30
+ });
31
+ }
32
+ function formatCategoryList(categories, countMap) {
33
+ const lines = categories.map((c) => {
34
+ const count = countMap.get(c.uid) ?? 0;
35
+ return `- **${c.name}** (${String(count)} ${count === 1 ? "recipe" : "recipes"})`;
36
+ });
37
+ return `## Recipe Categories\n\n${lines.join("\n")}`;
38
+ }
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { ServerContext } from "../types/server-context.js";
3
+ export declare function registerCreateTool(server: McpServer, ctx: ServerContext): void;
@@ -0,0 +1,79 @@
1
+ import { z } from "zod";
2
+ import { RecipeUidSchema } from "../paprika/types.js";
3
+ import { coldStartGuard, commitRecipe, recipeToMarkdown, resolveCategoryNames, textResult } from "./helpers.js";
4
+ export function registerCreateTool(server, ctx) {
5
+ server.registerTool("create_recipe", {
6
+ description: "Create a new recipe in the Paprika account.",
7
+ inputSchema: {
8
+ name: z.string().describe("Recipe name"),
9
+ ingredients: z.string().describe("Ingredients list"),
10
+ directions: z.string().describe("Cooking directions"),
11
+ description: z.string().optional().describe("Brief description"),
12
+ notes: z.string().optional().describe("Additional notes"),
13
+ servings: z.string().optional().describe("Number of servings"),
14
+ prepTime: z.string().optional().describe("Prep time (e.g. '15 min')"),
15
+ cookTime: z.string().optional().describe("Cook time (e.g. '30 min')"),
16
+ totalTime: z.string().optional().describe("Total time (e.g. '45 min')"),
17
+ categories: z.array(z.string()).optional().describe("Category display names (case-insensitive)"),
18
+ source: z.string().optional().describe("Source name"),
19
+ sourceUrl: z.string().optional().describe("Source URL"),
20
+ difficulty: z.string().optional().describe("Difficulty level"),
21
+ rating: z.number().int().min(0).max(5).optional().describe("Rating 0–5 (default: 0)"),
22
+ nutritionalInfo: z.string().optional().describe("Nutritional information"),
23
+ },
24
+ }, async (args) => {
25
+ return coldStartGuard(ctx).match(async () => {
26
+ // Resolve category names → UIDs (AC2.4, AC2.7)
27
+ const { uids: categories, unknown: unknownCategories } = args.categories && args.categories.length > 0
28
+ ? resolveCategoryNames(ctx.store.getAllCategories(), args.categories)
29
+ : { uids: [], unknown: [] };
30
+ const warnings = unknownCategories.map((name) => `Warning: category "${name}" not found and was skipped.`);
31
+ // Build the full Recipe object — all 28 fields required by the type
32
+ // hash: "" — Paprika API returns the real hash in the saveRecipe response
33
+ const uid = RecipeUidSchema.parse(crypto.randomUUID());
34
+ const newRecipe = {
35
+ uid,
36
+ hash: "",
37
+ name: args.name,
38
+ categories,
39
+ ingredients: args.ingredients,
40
+ directions: args.directions,
41
+ description: args.description ?? null, // AC2.3: omitted → null
42
+ notes: args.notes ?? null,
43
+ prepTime: args.prepTime ?? null,
44
+ cookTime: args.cookTime ?? null,
45
+ totalTime: args.totalTime ?? null,
46
+ servings: args.servings ?? null,
47
+ difficulty: args.difficulty ?? null,
48
+ rating: args.rating ?? 0, // AC2.3: omitted → 0 (Paprika's default)
49
+ created: new Date().toISOString(),
50
+ imageUrl: "",
51
+ photo: null,
52
+ photoHash: null,
53
+ photoLarge: null,
54
+ photoUrl: null,
55
+ source: args.source ?? null,
56
+ sourceUrl: args.sourceUrl ?? null,
57
+ onFavorites: false,
58
+ inTrash: false,
59
+ isPinned: false,
60
+ onGroceryList: false,
61
+ scale: null,
62
+ nutritionalInfo: args.nutritionalInfo ?? null,
63
+ };
64
+ let saved;
65
+ try {
66
+ saved = await ctx.client.saveRecipe(newRecipe); // AC2.5
67
+ await commitRecipe(ctx, saved); // AC2.5, AC2.6
68
+ }
69
+ catch (error) {
70
+ // AC2.8: store/cache not updated — commitRecipe not reached
71
+ return textResult(`Failed to create recipe: ${error instanceof Error ? error.message : String(error)}`);
72
+ }
73
+ const categoryNames = ctx.store.resolveCategories(saved.categories);
74
+ const markdown = recipeToMarkdown(saved, categoryNames);
75
+ const prefix = warnings.length > 0 ? warnings.join("\n") + "\n\n" : "";
76
+ return textResult(prefix + markdown);
77
+ }, (guard) => guard);
78
+ });
79
+ }
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { ServerContext } from "../types/server-context.js";
3
+ export declare function registerDeleteTool(server: McpServer, ctx: ServerContext): void;
@@ -0,0 +1,33 @@
1
+ import { z } from "zod";
2
+ import { RecipeUidSchema } from "../paprika/types.js";
3
+ import { coldStartGuard, commitRecipe, textResult } from "./helpers.js";
4
+ export function registerDeleteTool(server, ctx) {
5
+ server.registerTool("delete_recipe", {
6
+ description: "Soft-delete a recipe by UID, moving it to the Paprika trash. " +
7
+ "This operation is reversible — trashed recipes can be recovered in the Paprika app. " +
8
+ "Requires an exact UID; fuzzy title matching is not supported to prevent accidental deletion.",
9
+ inputSchema: {
10
+ uid: z.string().describe("Recipe UID to delete"),
11
+ },
12
+ }, async (args) => {
13
+ return coldStartGuard(ctx).match(async () => {
14
+ const uid = RecipeUidSchema.parse(args.uid);
15
+ const recipe = ctx.store.get(uid);
16
+ if (!recipe) {
17
+ return textResult(`No recipe found with UID "${args.uid}".`);
18
+ }
19
+ if (recipe.inTrash) {
20
+ return textResult(`Recipe "${recipe.name}" is already in the trash.`);
21
+ }
22
+ const trashed = { ...recipe, inTrash: true };
23
+ try {
24
+ const saved = await ctx.client.saveRecipe(trashed);
25
+ await commitRecipe(ctx, saved);
26
+ }
27
+ catch (error) {
28
+ return textResult(`Failed to delete recipe: ${error instanceof Error ? error.message : String(error)}`);
29
+ }
30
+ return textResult(`Recipe "${recipe.name}" has been moved to the trash.`);
31
+ }, (guard) => guard);
32
+ });
33
+ }
@@ -0,0 +1,4 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { ServerContext } from "../types/server-context.js";
3
+ import type { VectorStore } from "../features/vector-store.js";
4
+ export declare function registerDiscoverTool(server: McpServer, ctx: ServerContext, vectorStore: VectorStore): void;