@bojanrajkovic/mcp-paprika 1.2.0-beta.1 → 1.2.0-beta.2

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.
@@ -22,6 +22,20 @@ class TransientHTTPError extends Error {
22
22
  this.name = "TransientHTTPError";
23
23
  }
24
24
  }
25
+ // Private marker class used to route network-level fetch failures (DNS,
26
+ // TCP reset, TLS handshake, etc.) through cockatiel's handleType-based
27
+ // retry policy. undici throws a bare TypeError for these; the runtime
28
+ // has no dedicated subclass we can match on directly. The cause is the
29
+ // original TypeError so callers can unwrap and surface the real error
30
+ // once retries are exhausted.
31
+ class NetworkRetryableError extends Error {
32
+ cause;
33
+ constructor(cause) {
34
+ super(`Network error: ${cause.message}`, { cause });
35
+ this.cause = cause;
36
+ this.name = "NetworkRetryableError";
37
+ }
38
+ }
25
39
  class TokenExpiredError extends Error {
26
40
  constructor() {
27
41
  super("Token expired");
@@ -29,14 +43,14 @@ class TokenExpiredError extends Error {
29
43
  }
30
44
  }
31
45
  const RETRYABLE_STATUSES = new Set([429, 500, 502, 503]);
32
- const retryPolicy = retry(handleType(TransientHTTPError), {
46
+ const retryPolicy = retry(handleType(TransientHTTPError).orType(NetworkRetryableError), {
33
47
  maxAttempts: 3,
34
48
  backoff: new ExponentialBackoff({
35
49
  initialDelay: 500,
36
50
  maxDelay: 10_000,
37
51
  }),
38
52
  });
39
- const breakerPolicy = circuitBreaker(handleType(TransientHTTPError), {
53
+ const breakerPolicy = circuitBreaker(handleType(TransientHTTPError).orType(NetworkRetryableError), {
40
54
  halfOpenAfter: 30_000,
41
55
  breaker: new ConsecutiveBreaker(5),
42
56
  });
@@ -178,7 +192,20 @@ export class PaprikaClient {
178
192
  if (body !== undefined) {
179
193
  fetchInit.body = body;
180
194
  }
181
- const response = await fetch(url, fetchInit);
195
+ let response;
196
+ try {
197
+ response = await fetch(url, fetchInit);
198
+ }
199
+ catch (error) {
200
+ // undici throws a bare TypeError for network-level failures (DNS,
201
+ // TCP reset, TLS handshake, abort). Re-throw as a retryable marker
202
+ // so cockatiel's handleType policy applies the same backoff +
203
+ // circuit-breaker treatment as 5xx HTTP responses.
204
+ if (error instanceof TypeError) {
205
+ throw new NetworkRetryableError(error);
206
+ }
207
+ throw error;
208
+ }
182
209
  if (!response.ok) {
183
210
  if (RETRYABLE_STATUSES.has(response.status)) {
184
211
  throw new TransientHTTPError(response.status);
@@ -199,6 +226,11 @@ export class PaprikaClient {
199
226
  if (error instanceof BrokenCircuitError) {
200
227
  throw new PaprikaAPIError("Service unavailable (circuit open)", 503, url);
201
228
  }
229
+ // Unwrap the retry marker so callers see the original undici TypeError —
230
+ // tools that surface .message stay consistent with the pre-retry shape.
231
+ if (error instanceof NetworkRetryableError) {
232
+ throw error.cause;
233
+ }
202
234
  if (error instanceof TokenExpiredError) {
203
235
  if (!this.token) {
204
236
  throw new PaprikaAuthError("Authentication required (HTTP 401)");
@@ -214,6 +246,9 @@ export class PaprikaClient {
214
246
  if (retryError instanceof BrokenCircuitError) {
215
247
  throw new PaprikaAPIError("Service unavailable (circuit open)", 503, url);
216
248
  }
249
+ if (retryError instanceof NetworkRetryableError) {
250
+ throw retryError.cause;
251
+ }
217
252
  throw retryError;
218
253
  }
219
254
  }
@@ -1,7 +1,8 @@
1
- import { toMessage } from "../utils/log.js";
1
+ import { createLogger, toMessage } from "../utils/log.js";
2
2
  import { z } from "zod";
3
3
  import { RecipeUidSchema } from "../paprika/types.js";
4
4
  import { coldStartGuard, commitRecipe, recipeToMarkdown, resolveCategoryNames, textResult } from "./helpers.js";
5
+ const log = createLogger("mcp-paprika:create_recipe");
5
6
  export function registerCreateTool(server, ctx) {
6
7
  server.registerTool("create_recipe", {
7
8
  description: "Create a new recipe in the Paprika account.",
@@ -69,7 +70,9 @@ export function registerCreateTool(server, ctx) {
69
70
  }
70
71
  catch (error) {
71
72
  // AC2.8: store/cache not updated — commitRecipe not reached
72
- return textResult(`Failed to create recipe: ${toMessage(error)}`);
73
+ const message = toMessage(error);
74
+ log(`saveRecipe failed for name=${args.name}: ${message}`);
75
+ return textResult(`Failed to create recipe: ${message}`);
73
76
  }
74
77
  const categoryNames = ctx.store.resolveCategories(saved.categories);
75
78
  const markdown = recipeToMarkdown(saved, categoryNames);
@@ -1,7 +1,8 @@
1
- import { toMessage } from "../utils/log.js";
1
+ import { createLogger, toMessage } from "../utils/log.js";
2
2
  import { z } from "zod";
3
3
  import { RecipeUidSchema } from "../paprika/types.js";
4
4
  import { coldStartGuard, commitRecipe, textResult } from "./helpers.js";
5
+ const log = createLogger("mcp-paprika:delete_recipe");
5
6
  export function registerDeleteTool(server, ctx) {
6
7
  server.registerTool("delete_recipe", {
7
8
  description: "Soft-delete a recipe by UID, moving it to the Paprika trash. " +
@@ -26,7 +27,9 @@ export function registerDeleteTool(server, ctx) {
26
27
  await commitRecipe(ctx, saved);
27
28
  }
28
29
  catch (error) {
29
- return textResult(`Failed to delete recipe: ${toMessage(error)}`);
30
+ const message = toMessage(error);
31
+ log(`saveRecipe (soft-delete) failed for uid=${trashed.uid}: ${message}`);
32
+ return textResult(`Failed to delete recipe: ${message}`);
30
33
  }
31
34
  return textResult(`Recipe "${recipe.name}" has been moved to the trash.`);
32
35
  }, (guard) => guard);
@@ -1,10 +1,11 @@
1
1
  // pattern: Imperative Shell
2
- import { toMessage } from "../utils/log.js";
2
+ import { createLogger, toMessage } from "../utils/log.js";
3
3
  import { z } from "zod";
4
4
  import { PantryItemUidSchema } from "../paprika/types.js";
5
5
  import { normalizePaprikaDate, paprikaDateToday } from "../paprika/dates.js";
6
6
  import { textResult } from "./helpers.js";
7
7
  import { commitPantryItem, pantryItemToMarkdown, pantryStartGuard } from "./pantry-helpers.js";
8
+ const log = createLogger("mcp-paprika:add_pantry_item");
8
9
  export function registerAddPantryItemTool(server, ctx) {
9
10
  server.registerTool("add_pantry_item", {
10
11
  description: "Add a new item to the pantry. Rejects duplicates by case-insensitive ingredient name; " +
@@ -64,7 +65,9 @@ export function registerAddPantryItemTool(server, ctx) {
64
65
  }
65
66
  catch (error) {
66
67
  // AC4.7: store/cache not updated — commitPantryItem not reached
67
- return textResult(`Failed to add pantry item: ${toMessage(error)}`);
68
+ const message = toMessage(error);
69
+ log(`savePantryItem failed for ${args.ingredient}: ${message}`);
70
+ return textResult(`Failed to add pantry item: ${message}`);
68
71
  }
69
72
  return textResult(pantryItemToMarkdown(saved));
70
73
  }, (guard) => guard);
@@ -1,8 +1,9 @@
1
- import { toMessage } from "../utils/log.js";
1
+ import { createLogger, toMessage } from "../utils/log.js";
2
2
  import { z } from "zod";
3
3
  import { PantryItemUidSchema } from "../paprika/types.js";
4
4
  import { textResult } from "./helpers.js";
5
5
  import { commitPantryItem, pantryStartGuard } from "./pantry-helpers.js";
6
+ const log = createLogger("mcp-paprika:delete_pantry_item");
6
7
  export function registerDeletePantryItemTool(server, ctx) {
7
8
  server.registerTool("delete_pantry_item", {
8
9
  description: "Soft-delete a pantry item by UID. Idempotent: a second delete on the same UID " +
@@ -37,7 +38,9 @@ export function registerDeletePantryItemTool(server, ctx) {
37
38
  await commitPantryItem(ctx, saved);
38
39
  }
39
40
  catch (error) {
40
- return textResult(`Failed to delete pantry item: ${toMessage(error)}`);
41
+ const message = toMessage(error);
42
+ log(`savePantryItem (soft-delete) failed for uid=${trashed.uid}: ${message}`);
43
+ return textResult(`Failed to delete pantry item: ${message}`);
41
44
  }
42
45
  return textResult(`Pantry item "${existing.ingredient}" has been deleted.`);
43
46
  }, (guard) => guard);
@@ -1,9 +1,10 @@
1
- import { toMessage } from "../utils/log.js";
1
+ import { createLogger, toMessage } from "../utils/log.js";
2
2
  import { z } from "zod";
3
3
  import { PantryItemUidSchema } from "../paprika/types.js";
4
4
  import { normalizePaprikaDate } from "../paprika/dates.js";
5
5
  import { textResult } from "./helpers.js";
6
6
  import { commitPantryItem, pantryItemToMarkdown, pantryStartGuard } from "./pantry-helpers.js";
7
+ const log = createLogger("mcp-paprika:update_pantry_item");
7
8
  export function registerUpdatePantryItemTool(server, ctx) {
8
9
  server.registerTool("update_pantry_item", {
9
10
  description: "Update an existing pantry item by UID. Only provided fields are changed; " +
@@ -64,7 +65,9 @@ export function registerUpdatePantryItemTool(server, ctx) {
64
65
  await commitPantryItem(ctx, saved);
65
66
  }
66
67
  catch (error) {
67
- return textResult(`Failed to update pantry item: ${toMessage(error)}`);
68
+ const message = toMessage(error);
69
+ log(`savePantryItem failed for uid=${updated.uid}: ${message}`);
70
+ return textResult(`Failed to update pantry item: ${message}`);
68
71
  }
69
72
  return textResult(pantryItemToMarkdown(saved));
70
73
  }, (guard) => guard);
@@ -1,7 +1,8 @@
1
- import { toMessage } from "../utils/log.js";
1
+ import { createLogger, toMessage } from "../utils/log.js";
2
2
  import { z } from "zod";
3
3
  import { RecipeUidSchema } from "../paprika/types.js";
4
4
  import { coldStartGuard, commitRecipe, recipeToMarkdown, resolveCategoryNames, textResult } from "./helpers.js";
5
+ const log = createLogger("mcp-paprika:update_recipe");
5
6
  export function registerUpdateTool(server, ctx) {
6
7
  server.registerTool("update_recipe", {
7
8
  description: "Update an existing recipe by UID. Only provided fields are changed; " +
@@ -67,7 +68,9 @@ export function registerUpdateTool(server, ctx) {
67
68
  await commitRecipe(ctx, saved); // AC3.4
68
69
  }
69
70
  catch (error) {
70
- return textResult(`Failed to update recipe: ${toMessage(error)}`);
71
+ const message = toMessage(error);
72
+ log(`saveRecipe failed for uid=${updated.uid}: ${message}`);
73
+ return textResult(`Failed to update recipe: ${message}`);
71
74
  }
72
75
  const categoryNames = ctx.store.resolveCategories(saved.categories);
73
76
  const markdown = recipeToMarkdown(saved, categoryNames);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bojanrajkovic/mcp-paprika",
3
- "version": "1.2.0-beta.1",
3
+ "version": "1.2.0-beta.2",
4
4
  "description": "MCP server for Paprika recipe manager",
5
5
  "license": "MIT",
6
6
  "repository": {