@bojanrajkovic/mcp-paprika 2.0.0-beta.0 → 2.0.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.
- package/dist/auth/consent-page.d.ts +11 -5
- package/dist/auth/consent-page.js +30 -16
- package/dist/auth/provider.js +5 -1
- package/dist/tools/aisles.js +1 -0
- package/dist/tools/categories.js +1 -0
- package/dist/tools/category-writes.js +3 -0
- package/dist/tools/create.js +1 -0
- package/dist/tools/delete.js +1 -0
- package/dist/tools/discover.js +1 -0
- package/dist/tools/empty-trash.js +1 -0
- package/dist/tools/grocery-clear.js +2 -0
- package/dist/tools/grocery-item-purchase.js +1 -0
- package/dist/tools/grocery-item.js +3 -0
- package/dist/tools/grocery-list.js +5 -0
- package/dist/tools/grocery-move.js +1 -0
- package/dist/tools/list.js +1 -0
- package/dist/tools/meal-add-menu.js +1 -0
- package/dist/tools/meal-history-search.js +1 -0
- package/dist/tools/meal-log-cooked.js +1 -0
- package/dist/tools/meal-plan-read.js +1 -0
- package/dist/tools/meal-reschedule.js +1 -0
- package/dist/tools/meal-types.js +1 -0
- package/dist/tools/meal-writes.js +3 -0
- package/dist/tools/menu-item-move.js +1 -0
- package/dist/tools/menu-item-write.js +3 -0
- package/dist/tools/menu-read.js +2 -0
- package/dist/tools/menu-write.js +3 -0
- package/dist/tools/pantry-batch-add.js +1 -0
- package/dist/tools/pantry-delete.js +1 -0
- package/dist/tools/pantry-get.js +1 -0
- package/dist/tools/pantry-list.js +1 -0
- package/dist/tools/pantry-stock.js +2 -0
- package/dist/tools/pantry-update.js +1 -0
- package/dist/tools/photo-generate.js +1 -0
- package/dist/tools/photo-writes.js +2 -0
- package/dist/tools/read.js +1 -0
- package/dist/tools/recipe-categorize.js +1 -0
- package/dist/tools/recipe-favorite.js +2 -0
- package/dist/tools/recipe-rating.js +1 -0
- package/dist/tools/recipe-restore.js +1 -0
- package/dist/tools/search.js +1 -0
- package/dist/tools/update.js +1 -0
- package/package.json +1 -1
|
@@ -41,9 +41,15 @@ export declare function renderDeniedPage(): RenderedPage;
|
|
|
41
41
|
export declare function renderExpiredPage(): RenderedPage;
|
|
42
42
|
/**
|
|
43
43
|
* Security headers for every consent-flow response. The CSP pins inline styles
|
|
44
|
-
* to `nonce`, denies all other resource loads, restricts form submission
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
44
|
+
* to `nonce`, denies all other resource loads, restricts form submission, and
|
|
45
|
+
* forbids framing; `X-Frame-Options` backs frame-ancestors for older browsers;
|
|
46
|
+
* `Cache-Control` keeps the page (and its ticket) out of shared caches.
|
|
47
|
+
*
|
|
48
|
+
* `form-action` is enforced across redirects: the Allow form posts same-origin to
|
|
49
|
+
* `/oauth/consent`, which 302-redirects to the upstream IdP authorize endpoint,
|
|
50
|
+
* so `'self'` alone blocks the approve navigation. Callers rendering the consent
|
|
51
|
+
* *form* must pass `formActionOrigin` (the IdP authorize-endpoint origin) so that
|
|
52
|
+
* redirect is allowed; terminal pages (denied/expired) carry no form and omit it,
|
|
53
|
+
* keeping `form-action 'self'`.
|
|
48
54
|
*/
|
|
49
|
-
export declare function consentSecurityHeaders(nonce: string): Record<string, string>;
|
|
55
|
+
export declare function consentSecurityHeaders(nonce: string, formActionOrigin?: string): Record<string, string>;
|
|
@@ -34,12 +34,19 @@ function escapeHtml(value) {
|
|
|
34
34
|
function generateNonce() {
|
|
35
35
|
return randomBytes(16).toString("base64");
|
|
36
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* The authorization server's display name in the consent brandbar and page
|
|
39
|
+
* titles. This is the OAuth-surface identity (distinct from the host-facing
|
|
40
|
+
* connector card name in `src/utils/branding.ts`); a connecting user reads it as
|
|
41
|
+
* "the Paprika MCP Connector wants to connect."
|
|
42
|
+
*/
|
|
43
|
+
const DISPLAY_NAME = "Paprika MCP Connector";
|
|
37
44
|
const STYLES = `
|
|
38
45
|
:root {
|
|
39
46
|
--paper: oklch(0.985 0.006 75); --card: oklch(0.995 0.004 75);
|
|
40
47
|
--ink: oklch(0.26 0.012 70); --muted: oklch(0.53 0.012 70); --faint: oklch(0.66 0.010 70);
|
|
41
48
|
--line: oklch(0.90 0.008 70); --line-2: oklch(0.83 0.010 70);
|
|
42
|
-
--clay: oklch(0.
|
|
49
|
+
--clay: oklch(0.543 0.174 30); --clay-ink: oklch(0.99 0.01 75); --dest-bg: oklch(0.965 0.012 70); /* --clay = #C0392B, the connector icon color */
|
|
43
50
|
--mono: ui-monospace, "SF Mono", Menlo, monospace;
|
|
44
51
|
--sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
45
52
|
}
|
|
@@ -69,14 +76,14 @@ const STYLES = `
|
|
|
69
76
|
.grants .lbl { font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.06em;
|
|
70
77
|
color: var(--faint); margin-bottom: 4px; }
|
|
71
78
|
.grants p { margin: 0; font-size: 0.9rem; color: var(--muted); }
|
|
72
|
-
.denyline { font-size: 0.82rem; color: var(--muted); padding: 14px
|
|
79
|
+
.denyline { font-size: 0.82rem; color: var(--muted); padding: 14px 0 0; }
|
|
73
80
|
.denyline b { color: var(--ink); }
|
|
74
|
-
.actions { display: flex; gap: 10px; padding: 14px
|
|
81
|
+
.actions { display: flex; gap: 10px; padding: 14px 0 20px; }
|
|
75
82
|
.btn { flex: 1; text-align: center; padding: 11px 14px; border-radius: 9px; font: inherit;
|
|
76
83
|
font-size: 0.92rem; font-weight: 600; border: 1px solid transparent; cursor: pointer; }
|
|
77
84
|
.btn-ghost { background: transparent; color: var(--ink); border-color: var(--line-2); }
|
|
78
85
|
.btn-fill { background: var(--clay); color: var(--clay-ink); }
|
|
79
|
-
.foot { padding: 0
|
|
86
|
+
.foot { padding: 0 0 18px; font-size: 0.78rem; color: var(--faint); }
|
|
80
87
|
.terminal { padding: 28px 24px; text-align: center; }
|
|
81
88
|
.terminal h1 { font-size: 1.2rem; font-weight: 650; margin: 0 0 8px; }
|
|
82
89
|
.terminal p { margin: 0; color: var(--muted); font-size: 0.9rem; }
|
|
@@ -108,7 +115,7 @@ export function renderConsentPage(params) {
|
|
|
108
115
|
const safeTicket = escapeHtml(params.ticket);
|
|
109
116
|
const initial = escapeHtml(name.charAt(0).toUpperCase() || "?");
|
|
110
117
|
const body = `<main class="screen">
|
|
111
|
-
<div class="brandbar"><span class="dot"></span>
|
|
118
|
+
<div class="brandbar"><span class="dot"></span> ${DISPLAY_NAME}</div>
|
|
112
119
|
<form class="body" method="post" action="/oauth/consent">
|
|
113
120
|
<input type="hidden" name="ticket" value="${safeTicket}">
|
|
114
121
|
<div class="head">
|
|
@@ -134,45 +141,52 @@ export function renderConsentPage(params) {
|
|
|
134
141
|
<div class="foot">You'll sign in after allowing. Only you can complete this.</div>
|
|
135
142
|
</form>
|
|
136
143
|
</main>`;
|
|
137
|
-
return { html: shell(nonce,
|
|
144
|
+
return { html: shell(nonce, `Authorize access — ${DISPLAY_NAME}`, body), nonce };
|
|
138
145
|
}
|
|
139
146
|
/** Terminal page shown on Deny. Stays on our origin; never redirects to the redirect_uri. */
|
|
140
147
|
export function renderDeniedPage() {
|
|
141
148
|
const nonce = generateNonce();
|
|
142
149
|
const body = `<main class="screen">
|
|
143
|
-
<div class="brandbar"><span class="dot"></span>
|
|
150
|
+
<div class="brandbar"><span class="dot"></span> ${DISPLAY_NAME}</div>
|
|
144
151
|
<div class="terminal">
|
|
145
152
|
<h1>Access denied</h1>
|
|
146
153
|
<p>No connection was authorized. You can close this tab.</p>
|
|
147
154
|
</div>
|
|
148
155
|
</main>`;
|
|
149
|
-
return { html: shell(nonce,
|
|
156
|
+
return { html: shell(nonce, `Access denied — ${DISPLAY_NAME}`, body), nonce };
|
|
150
157
|
}
|
|
151
158
|
/** Terminal page shown when a consent ticket is unknown or expired. */
|
|
152
159
|
export function renderExpiredPage() {
|
|
153
160
|
const nonce = generateNonce();
|
|
154
161
|
const body = `<main class="screen">
|
|
155
|
-
<div class="brandbar"><span class="dot"></span>
|
|
162
|
+
<div class="brandbar"><span class="dot"></span> ${DISPLAY_NAME}</div>
|
|
156
163
|
<div class="terminal">
|
|
157
164
|
<h1>Request expired</h1>
|
|
158
165
|
<p>This authorization request is no longer valid. Start the connection again from your app.</p>
|
|
159
166
|
</div>
|
|
160
167
|
</main>`;
|
|
161
|
-
return { html: shell(nonce,
|
|
168
|
+
return { html: shell(nonce, `Request expired — ${DISPLAY_NAME}`, body), nonce };
|
|
162
169
|
}
|
|
163
170
|
/**
|
|
164
171
|
* Security headers for every consent-flow response. The CSP pins inline styles
|
|
165
|
-
* to `nonce`, denies all other resource loads, restricts form submission
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
172
|
+
* to `nonce`, denies all other resource loads, restricts form submission, and
|
|
173
|
+
* forbids framing; `X-Frame-Options` backs frame-ancestors for older browsers;
|
|
174
|
+
* `Cache-Control` keeps the page (and its ticket) out of shared caches.
|
|
175
|
+
*
|
|
176
|
+
* `form-action` is enforced across redirects: the Allow form posts same-origin to
|
|
177
|
+
* `/oauth/consent`, which 302-redirects to the upstream IdP authorize endpoint,
|
|
178
|
+
* so `'self'` alone blocks the approve navigation. Callers rendering the consent
|
|
179
|
+
* *form* must pass `formActionOrigin` (the IdP authorize-endpoint origin) so that
|
|
180
|
+
* redirect is allowed; terminal pages (denied/expired) carry no form and omit it,
|
|
181
|
+
* keeping `form-action 'self'`.
|
|
169
182
|
*/
|
|
170
|
-
export function consentSecurityHeaders(nonce) {
|
|
183
|
+
export function consentSecurityHeaders(nonce, formActionOrigin) {
|
|
184
|
+
const formAction = formActionOrigin ? `form-action 'self' ${formActionOrigin}` : "form-action 'self'";
|
|
171
185
|
return {
|
|
172
186
|
"Content-Security-Policy": [
|
|
173
187
|
"default-src 'none'",
|
|
174
188
|
`style-src 'nonce-${nonce}'`,
|
|
175
|
-
|
|
189
|
+
formAction,
|
|
176
190
|
"base-uri 'none'",
|
|
177
191
|
"frame-ancestors 'none'",
|
|
178
192
|
].join("; "),
|
package/dist/auth/provider.js
CHANGED
|
@@ -83,7 +83,11 @@ export class MintingOAuthServerProvider {
|
|
|
83
83
|
...clientNameProp,
|
|
84
84
|
redirectHost: new URL(params.redirectUri).origin,
|
|
85
85
|
});
|
|
86
|
-
|
|
86
|
+
// The Allow form posts to /oauth/consent, which 302s to the upstream IdP.
|
|
87
|
+
// form-action is enforced across that redirect, so the IdP origin must be
|
|
88
|
+
// allowed or the browser blocks the approve navigation (consuming the ticket).
|
|
89
|
+
const idpOrigin = new URL(this._discovery.authorization_endpoint).origin;
|
|
90
|
+
c.res = c.html(html, 200, consentSecurityHeaders(nonce, idpOrigin));
|
|
87
91
|
}
|
|
88
92
|
/** Narrow deps bundle for the shared `redirectUpstream` helper. */
|
|
89
93
|
_upstreamRedirectDeps() {
|
package/dist/tools/aisles.js
CHANGED
|
@@ -3,6 +3,7 @@ import { textResult } from "./helpers.js";
|
|
|
3
3
|
export function registerAislesTool(server, ctx) {
|
|
4
4
|
const log = ctx.log.child({ component: "list_aisles" });
|
|
5
5
|
server.registerTool("list_aisles", {
|
|
6
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
6
7
|
description: "List all known aisles, sorted by order then name. " +
|
|
7
8
|
"Includes the aisle UID needed for pantry and grocery item writes.",
|
|
8
9
|
inputSchema: {},
|
package/dist/tools/categories.js
CHANGED
|
@@ -3,6 +3,7 @@ import { textResult } from "./helpers.js";
|
|
|
3
3
|
export function registerCategoryTools(server, ctx) {
|
|
4
4
|
const log = ctx.log.child({ component: "list_categories" });
|
|
5
5
|
server.registerTool("list_categories", {
|
|
6
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
6
7
|
description: "List all recipe categories with the number of recipes in each. Categories are sorted alphabetically.",
|
|
7
8
|
inputSchema: {},
|
|
8
9
|
}, async (_args) => {
|
|
@@ -11,6 +11,7 @@ function categorySummary(ctx, category) {
|
|
|
11
11
|
export function registerCreateCategoryTool(server, ctx) {
|
|
12
12
|
const log = ctx.log.child({ component: "create_category" });
|
|
13
13
|
server.registerTool("create_category", {
|
|
14
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
|
|
14
15
|
description: "Create a new recipe category. Optionally nest it under an existing category by passing that " +
|
|
15
16
|
"category's UID as `parentUid` to build a hierarchy (e.g. Thai → Curries). Use `list_categories` " +
|
|
16
17
|
"to find parent UIDs. To put recipes in the new category, follow up with `update_recipe`.",
|
|
@@ -45,6 +46,7 @@ export function registerCreateCategoryTool(server, ctx) {
|
|
|
45
46
|
export function registerUpdateCategoryTool(server, ctx) {
|
|
46
47
|
const log = ctx.log.child({ component: "update_category" });
|
|
47
48
|
server.registerTool("update_category", {
|
|
49
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true },
|
|
48
50
|
description: "Rename and/or re-parent an existing category. Pass `name` to rename, `parentUid` to move it under " +
|
|
49
51
|
"another category, or `null` for `parentUid` to make it top-level. Re-parenting builds the hierarchy " +
|
|
50
52
|
"that `list_categories` renders.",
|
|
@@ -99,6 +101,7 @@ export function registerUpdateCategoryTool(server, ctx) {
|
|
|
99
101
|
export function registerDeleteCategoryTool(server, ctx) {
|
|
100
102
|
const log = ctx.log.child({ component: "delete_category" });
|
|
101
103
|
server.registerTool("delete_category", {
|
|
104
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true },
|
|
102
105
|
description: "Delete a category. Refuses if the category still has child categories or is assigned to any recipe — " +
|
|
103
106
|
"reassign or delete those first (move recipes with `update_recipe`, re-parent children with " +
|
|
104
107
|
"`update_category`). This keeps the hierarchy and recipe links consistent.",
|
package/dist/tools/create.js
CHANGED
|
@@ -6,6 +6,7 @@ import { coldStartGuard, commitRecipe, recipeToMarkdown, resolveCategoryRefs, te
|
|
|
6
6
|
export function registerCreateTool(server, ctx) {
|
|
7
7
|
const log = ctx.log.child({ component: "create_recipe" });
|
|
8
8
|
server.registerTool("create_recipe", {
|
|
9
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
|
|
9
10
|
description: "Create a new recipe in the Paprika account. If you built this recipe from a web page, " +
|
|
10
11
|
"follow up with `upload_recipe_photo` and the page's main/hero (og:image) image URL to attach its photo.",
|
|
11
12
|
inputSchema: {
|
package/dist/tools/delete.js
CHANGED
|
@@ -4,6 +4,7 @@ import { coldStartGuard, commitRecipe, textResult } from "./helpers.js";
|
|
|
4
4
|
export function registerDeleteTool(server, ctx) {
|
|
5
5
|
const log = ctx.log.child({ component: "trash_recipe" });
|
|
6
6
|
server.registerTool("trash_recipe", {
|
|
7
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false },
|
|
7
8
|
description: "Soft-delete a recipe by UID, moving it to the Paprika trash. " +
|
|
8
9
|
"This operation is reversible — trashed recipes can be recovered in the Paprika app. " +
|
|
9
10
|
"Requires an exact UID; fuzzy title matching is not supported to prevent accidental deletion.",
|
package/dist/tools/discover.js
CHANGED
|
@@ -3,6 +3,7 @@ import { coldStartGuard, recipeMetadataLines, textResult } from "./helpers.js";
|
|
|
3
3
|
export function registerDiscoverTool(server, ctx, vectorStore) {
|
|
4
4
|
const log = ctx.log.child({ component: "discover_recipes" });
|
|
5
5
|
server.registerTool("discover_recipes", {
|
|
6
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
6
7
|
description: "Discover recipes using semantic search. Finds recipes matching a natural language description of what you're looking for.",
|
|
7
8
|
inputSchema: {
|
|
8
9
|
query: z.string().describe("Natural language description of what you're looking for"),
|
|
@@ -5,6 +5,7 @@ import { coldStartGuard, commitRecipeHardDelete, reconcileLocalRecipe, reconcile
|
|
|
5
5
|
export function registerEmptyTrashTool(server, ctx) {
|
|
6
6
|
const log = ctx.log.child({ component: "purge_recipe" });
|
|
7
7
|
server.registerTool("purge_recipe", {
|
|
8
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false },
|
|
8
9
|
description: "Permanently delete a recipe that is already in the Paprika trash. " +
|
|
9
10
|
"This is IRREVERSIBLE — once emptied from the trash the recipe cannot be recovered. " +
|
|
10
11
|
"The recipe must first be moved to the trash with trash_recipe (a reversible soft-delete); " +
|
|
@@ -5,6 +5,7 @@ import { textResult } from "./helpers.js";
|
|
|
5
5
|
export function registerClearPurchasedTool(server, ctx) {
|
|
6
6
|
const log = ctx.log.child({ component: "clear_purchased_grocery_items" });
|
|
7
7
|
server.registerTool("clear_purchased_grocery_items", {
|
|
8
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true },
|
|
8
9
|
description: "Clear all purchased items from a grocery list.",
|
|
9
10
|
inputSchema: {
|
|
10
11
|
listUid: GroceryListUidSchema.describe("Grocery list UID to clear purchased items from"),
|
|
@@ -37,6 +38,7 @@ export function registerClearPurchasedTool(server, ctx) {
|
|
|
37
38
|
export function registerClearAllTool(server, ctx) {
|
|
38
39
|
const log = ctx.log.child({ component: "clear_grocery_list" });
|
|
39
40
|
server.registerTool("clear_grocery_list", {
|
|
41
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true },
|
|
40
42
|
description: "Clear all items from a grocery list.",
|
|
41
43
|
inputSchema: {
|
|
42
44
|
listUid: GroceryListUidSchema.describe("Grocery list UID to clear all items from"),
|
|
@@ -11,6 +11,7 @@ export const markGroceryItemPurchasedInputSchema = z
|
|
|
11
11
|
export function registerMarkGroceryItemPurchasedTool(server, ctx) {
|
|
12
12
|
const log = ctx.log.child({ component: "mark_grocery_item_purchased" });
|
|
13
13
|
server.registerTool("mark_grocery_item_purchased", {
|
|
14
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },
|
|
14
15
|
description: "Mark a grocery item as purchased (checked off) by UID.",
|
|
15
16
|
inputSchema: markGroceryItemPurchasedInputSchema,
|
|
16
17
|
}, async (args) => {
|
|
@@ -17,6 +17,7 @@ const itemInputSchema = z.object({
|
|
|
17
17
|
export function registerAddGroceryItemsTool(server, ctx) {
|
|
18
18
|
const log = ctx.log.child({ component: "add_grocery_items" });
|
|
19
19
|
server.registerTool("add_grocery_items", {
|
|
20
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
|
|
20
21
|
description: "Add one or more items to a grocery list. Check read_grocery_list first to avoid duplicate ingredients — no server-side duplicate guard.",
|
|
21
22
|
inputSchema: {
|
|
22
23
|
listUid: GroceryListUidSchema.describe("UID of the grocery list to add items to"),
|
|
@@ -150,6 +151,7 @@ export const updateGroceryItemInputSchema = z
|
|
|
150
151
|
export function registerUpdateGroceryItemTool(server, ctx) {
|
|
151
152
|
const log = ctx.log.child({ component: "update_grocery_item" });
|
|
152
153
|
server.registerTool("update_grocery_item", {
|
|
154
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true },
|
|
153
155
|
description: "Update a grocery item's quantity, aisle, or notes by UID. Only provided fields are changed; " +
|
|
154
156
|
"omitted fields retain their current values. To check an item off, use mark_grocery_item_purchased.",
|
|
155
157
|
inputSchema: updateGroceryItemInputSchema,
|
|
@@ -188,6 +190,7 @@ export function registerUpdateGroceryItemTool(server, ctx) {
|
|
|
188
190
|
export function registerDeleteGroceryItemTool(server, ctx) {
|
|
189
191
|
const log = ctx.log.child({ component: "delete_grocery_item" });
|
|
190
192
|
server.registerTool("delete_grocery_item", {
|
|
193
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true },
|
|
191
194
|
description: "Delete a grocery item by UID.",
|
|
192
195
|
inputSchema: {
|
|
193
196
|
uid: GroceryItemUidSchema.describe("Grocery item UID to delete"),
|
|
@@ -6,6 +6,7 @@ import { formatLookupOutcome, resolveLookup, textResult, uidOrTextLookupSchema }
|
|
|
6
6
|
export function registerListGroceryListsTool(server, ctx) {
|
|
7
7
|
const log = ctx.log.child({ component: "list_grocery_lists" });
|
|
8
8
|
server.registerTool("list_grocery_lists", {
|
|
9
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
9
10
|
description: "List all grocery lists sorted alphabetically by name, with UID and item count per list.",
|
|
10
11
|
inputSchema: {},
|
|
11
12
|
}, async () => {
|
|
@@ -28,6 +29,7 @@ export function registerListGroceryListsTool(server, ctx) {
|
|
|
28
29
|
export function registerReadGroceryListTool(server, ctx) {
|
|
29
30
|
const log = ctx.log.child({ component: "read_grocery_list" });
|
|
30
31
|
server.registerTool("read_grocery_list", {
|
|
32
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
31
33
|
description: "Get a grocery list by UID or name. Name lookup is tiered (exact → starts-with → contains) " +
|
|
32
34
|
"and case-insensitive, with a disambiguation list when multiple lists match the same tier. " +
|
|
33
35
|
'Pass exactly one shape: {"uid": "..."} or {"name": "..."}.',
|
|
@@ -58,6 +60,7 @@ export function registerReadGroceryListTool(server, ctx) {
|
|
|
58
60
|
export function registerCreateGroceryListTool(server, ctx) {
|
|
59
61
|
const log = ctx.log.child({ component: "create_grocery_list" });
|
|
60
62
|
server.registerTool("create_grocery_list", {
|
|
63
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
|
|
61
64
|
description: "Create a new grocery list with the given name. Rejects duplicate names (case-insensitive exact match); " +
|
|
62
65
|
"if a duplicate is found, the response includes the existing UID.",
|
|
63
66
|
inputSchema: {
|
|
@@ -99,6 +102,7 @@ export function registerCreateGroceryListTool(server, ctx) {
|
|
|
99
102
|
export function registerRenameGroceryListTool(server, ctx) {
|
|
100
103
|
const log = ctx.log.child({ component: "rename_grocery_list" });
|
|
101
104
|
server.registerTool("rename_grocery_list", {
|
|
105
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },
|
|
102
106
|
description: "Rename a grocery list. Rejects if the new name conflicts with a different existing list.",
|
|
103
107
|
inputSchema: {
|
|
104
108
|
uid: GroceryListUidSchema.describe("Grocery list UID to rename"),
|
|
@@ -140,6 +144,7 @@ export function registerRenameGroceryListTool(server, ctx) {
|
|
|
140
144
|
export function registerDeleteGroceryListTool(server, ctx) {
|
|
141
145
|
const log = ctx.log.child({ component: "delete_grocery_list" });
|
|
142
146
|
server.registerTool("delete_grocery_list", {
|
|
147
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true },
|
|
143
148
|
description: "Delete a grocery list by UID.",
|
|
144
149
|
inputSchema: {
|
|
145
150
|
uid: GroceryListUidSchema.describe("Grocery list UID to delete"),
|
|
@@ -8,6 +8,7 @@ import { commitPantryItemsBatch } from "./pantry-helpers.js";
|
|
|
8
8
|
export function registerMoveToPantryTool(server, ctx) {
|
|
9
9
|
const log = ctx.log.child({ component: "move_grocery_items_to_pantry" });
|
|
10
10
|
server.registerTool("move_grocery_items_to_pantry", {
|
|
11
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false },
|
|
11
12
|
description: "Move one or more grocery items to the pantry. Creates pantry items (with today's purchase date), then deletes the grocery items.",
|
|
12
13
|
inputSchema: {
|
|
13
14
|
uids: z.array(GroceryItemUidSchema).min(1).describe("Grocery item UIDs to move to pantry"),
|
package/dist/tools/list.js
CHANGED
|
@@ -3,6 +3,7 @@ import { coldStartGuard, textResult } from "./helpers.js";
|
|
|
3
3
|
export function registerListTool(server, ctx) {
|
|
4
4
|
const log = ctx.log.child({ component: "list_recipes" });
|
|
5
5
|
server.registerTool("list_recipes", {
|
|
6
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
6
7
|
description: "List all recipes with pagination. Returns recipe summaries sorted alphabetically. Use offset/limit to paginate through the full library. Response includes total recipe count.",
|
|
7
8
|
inputSchema: {
|
|
8
9
|
offset: z.number().int().nonnegative().optional().default(0).describe("Number of recipes to skip (default: 0)"),
|
|
@@ -56,6 +56,7 @@ function renderPlannerAdds(menuName, startDay, items) {
|
|
|
56
56
|
export function registerAddMenuToPlannerTool(server, ctx) {
|
|
57
57
|
const log = ctx.log.child({ component: "schedule_menu" });
|
|
58
58
|
server.registerTool("schedule_menu", {
|
|
59
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
|
|
59
60
|
description: "Instantiate a saved menu's recipes as meal-planner entries. Look the menu up by UID or name " +
|
|
60
61
|
"(tiered fuzzy match), then materialize each of its items into a meal dated start_date + (day − 1) " +
|
|
61
62
|
"days, posting them all in one batch. This is a one-way COPY, not a link: the planner meals carry no " +
|
|
@@ -30,6 +30,7 @@ export const searchMealHistoryInputSchema = z
|
|
|
30
30
|
export function registerSearchMealHistoryTool(server, ctx) {
|
|
31
31
|
const log = ctx.log.child({ component: "search_meal_history" });
|
|
32
32
|
server.registerTool("search_meal_history", {
|
|
33
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
33
34
|
description: 'Search PAST meals (recall/browse), by a specific recipe, a recipe category ("class"), a meal type, ' +
|
|
34
35
|
'and/or a date window — any combination, ANDed. Answers "when did we last have tacos", "how often do ' +
|
|
35
36
|
'we eat Italian", "show the dinners we had in March", or "what have we eaten lately". With no filters ' +
|
|
@@ -21,6 +21,7 @@ export const logCookedMealInputSchema = z
|
|
|
21
21
|
export function registerLogCookedMealTool(server, ctx) {
|
|
22
22
|
const log = ctx.log.child({ component: "log_cooked_meal" });
|
|
23
23
|
server.registerTool("log_cooked_meal", {
|
|
24
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
|
|
24
25
|
description: "Log a meal you cooked: records the given recipe on the planner, defaulting to today and the Dinner " +
|
|
25
26
|
"meal type — a quick way to keep your cooking history current. Pass `date` to log a different day or " +
|
|
26
27
|
"`type` for a non-dinner meal. To log a freeform (non-recipe) meal or to plan ahead in bulk, use plan_meals.",
|
|
@@ -16,6 +16,7 @@ export const readMealPlanInputSchema = z
|
|
|
16
16
|
export function registerReadMealPlanTool(server, ctx) {
|
|
17
17
|
const log = ctx.log.child({ component: "read_meal_plan" });
|
|
18
18
|
server.registerTool("read_meal_plan", {
|
|
19
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
19
20
|
description: "Read the upcoming meal plan: meals scheduled from today forward, grouped by day in ascending date " +
|
|
20
21
|
'order (today first). Defaults to the next 7 days; pass `days` to widen the window. For past meals or recall ("when did we last have X"), use search_meal_history.',
|
|
21
22
|
inputSchema: readMealPlanInputSchema,
|
|
@@ -24,6 +24,7 @@ export const rescheduleMealInputSchema = z
|
|
|
24
24
|
export function registerRescheduleMealTool(server, ctx) {
|
|
25
25
|
const log = ctx.log.child({ component: "reschedule_meal" });
|
|
26
26
|
server.registerTool("reschedule_meal", {
|
|
27
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },
|
|
27
28
|
description: "Reschedule a planned meal to a different date by UID, optionally also changing its meal type. " +
|
|
28
29
|
"Moving the date re-sequences the meal to the end of the destination day's order. To change a " +
|
|
29
30
|
"meal's recipe link, freeform name, or scale instead, use update_meal.",
|
package/dist/tools/meal-types.js
CHANGED
|
@@ -35,6 +35,7 @@ function mealTypeLine(mt) {
|
|
|
35
35
|
export function registerMealTypesTool(server, ctx) {
|
|
36
36
|
const log = ctx.log.child({ component: "list_meal_types" });
|
|
37
37
|
server.registerTool("list_meal_types", {
|
|
38
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
38
39
|
description: "List all meal types — the built-in Breakfast/Lunch/Dinner/Snacks plus any custom " +
|
|
39
40
|
"types — sorted by order then name. Each entry shows whether it is built-in or custom, " +
|
|
40
41
|
"its calendar-export schedule (all-day or a clock time), and its UID. Reference a type " +
|
|
@@ -55,6 +55,7 @@ export const addMealsInputSchema = z.object({
|
|
|
55
55
|
export function registerAddMealsTool(server, ctx) {
|
|
56
56
|
const log = ctx.log.child({ component: "plan_meals" });
|
|
57
57
|
server.registerTool("plan_meals", {
|
|
58
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
|
|
58
59
|
description: "Add one or more meals to the meal planner. Each item is EITHER recipe-linked (supply " +
|
|
59
60
|
"recipe_uid; display name auto-resolves from the recipe) OR freeform (supply name; no " +
|
|
60
61
|
"recipe). The two shapes are mutually exclusive — Paprika.app's UI dispatches display " +
|
|
@@ -226,6 +227,7 @@ export const updateMealInputSchema = z.object({
|
|
|
226
227
|
export function registerUpdateMealTool(server, ctx) {
|
|
227
228
|
const log = ctx.log.child({ component: "update_meal" });
|
|
228
229
|
server.registerTool("update_meal", {
|
|
230
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true },
|
|
229
231
|
description: "Update an existing meal by UID. The `update` payload is a discriminated union: pick exactly one " +
|
|
230
232
|
"of {recipe_uid?, ...other} | {name, ...other} | {recipe_uid: null, name?, ...other}. Recipe link " +
|
|
231
233
|
"and display name are structurally exclusive: name auto-resolves from the recipe for linked meals, " +
|
|
@@ -368,6 +370,7 @@ const deleteMealInputSchema = z.object({
|
|
|
368
370
|
export function registerDeleteMealTool(server, ctx) {
|
|
369
371
|
const log = ctx.log.child({ component: "delete_meal" });
|
|
370
372
|
server.registerTool("delete_meal", {
|
|
373
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true },
|
|
371
374
|
description: "Soft-delete a meal from the planner by UID. Idempotent: a second delete on the same UID " +
|
|
372
375
|
"returns a friendly 'already deleted' message without re-POSTing. Requires an exact UID.",
|
|
373
376
|
inputSchema: deleteMealInputSchema.shape,
|
|
@@ -20,6 +20,7 @@ export const moveMenuItemInputSchema = z
|
|
|
20
20
|
export function registerMoveMenuItemTool(server, ctx) {
|
|
21
21
|
const log = ctx.log.child({ component: "move_menu_item" });
|
|
22
22
|
server.registerTool("move_menu_item", {
|
|
23
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },
|
|
23
24
|
description: "Move a menu item to a different day within its menu, by UID. A day beyond the menu's current span " +
|
|
24
25
|
"auto-extends the menu so the item stays visible, and the item is re-sequenced to the end of the " +
|
|
25
26
|
"menu's order. To change a menu item's meal type or recipe link instead, use update_menu_item.",
|
|
@@ -47,6 +47,7 @@ export const addMenuItemsInputSchema = z.object({
|
|
|
47
47
|
export function registerAddMenuItemsTool(server, ctx) {
|
|
48
48
|
const log = ctx.log.child({ component: "add_menu_items" });
|
|
49
49
|
server.registerTool("add_menu_items", {
|
|
50
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
|
|
50
51
|
description: "Add one or more menuitems to a menu (saved meal plan). Look the menu up by UID or name (tiered " +
|
|
51
52
|
"fuzzy match). Each item is EITHER recipe-linked (supply recipe_uid; display name auto-resolves " +
|
|
52
53
|
"from the recipe) OR freeform (supply name; no recipe) — the two are mutually exclusive, matching " +
|
|
@@ -205,6 +206,7 @@ export const updateMenuItemInputSchema = z
|
|
|
205
206
|
export function registerUpdateMenuItemTool(server, ctx) {
|
|
206
207
|
const log = ctx.log.child({ component: "update_menu_item" });
|
|
207
208
|
server.registerTool("update_menu_item", {
|
|
209
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true },
|
|
208
210
|
description: "Update an existing menuitem's meal type or recipe link by UID. Provide at least one of type or " +
|
|
209
211
|
"recipe_uid; omitted fields keep their current values. Changing recipe_uid re-resolves the display " +
|
|
210
212
|
"name from the new recipe. To move an item to a different day, use move_menu_item. The menu link " +
|
|
@@ -283,6 +285,7 @@ export const deleteMenuItemInputSchema = z.object({
|
|
|
283
285
|
export function registerDeleteMenuItemTool(server, ctx) {
|
|
284
286
|
const log = ctx.log.child({ component: "delete_menu_item" });
|
|
285
287
|
server.registerTool("delete_menu_item", {
|
|
288
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true },
|
|
286
289
|
description: "Soft-delete a menuitem (a planned recipe) from a menu by UID. Idempotent: a second delete on the " +
|
|
287
290
|
"same UID returns a friendly 'already deleted' message without re-POSTing. Requires an exact UID.",
|
|
288
291
|
inputSchema: deleteMenuItemInputSchema.shape,
|
package/dist/tools/menu-read.js
CHANGED
|
@@ -4,6 +4,7 @@ import { menuStartGuard, menuToMarkdown } from "./menu-helpers.js";
|
|
|
4
4
|
export function registerListMenusTool(server, ctx) {
|
|
5
5
|
const log = ctx.log.child({ component: "list_menus" });
|
|
6
6
|
server.registerTool("list_menus", {
|
|
7
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
7
8
|
description: "List all menus (saved meal plans) in Paprika order, with item count and day span per menu. " +
|
|
8
9
|
"Use read_menu to see a menu's full day-by-day breakdown.",
|
|
9
10
|
inputSchema: {},
|
|
@@ -26,6 +27,7 @@ export function registerListMenusTool(server, ctx) {
|
|
|
26
27
|
export function registerReadMenuTool(server, ctx) {
|
|
27
28
|
const log = ctx.log.child({ component: "read_menu" });
|
|
28
29
|
server.registerTool("read_menu", {
|
|
30
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
29
31
|
description: "Get a menu by UID or name, rendered day by day with each day's planned recipes. " +
|
|
30
32
|
"Name lookup is tiered (exact → starts-with → contains) and case-insensitive, with a " +
|
|
31
33
|
"disambiguation list when multiple menus match the same tier. Each recipe line carries " +
|
package/dist/tools/menu-write.js
CHANGED
|
@@ -6,6 +6,7 @@ import { commitMenu, commitMenuItemsBatch, menuStartGuard, menuToMarkdown } from
|
|
|
6
6
|
export function registerCreateMenuTool(server, ctx) {
|
|
7
7
|
const log = ctx.log.child({ component: "create_menu" });
|
|
8
8
|
server.registerTool("create_menu", {
|
|
9
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
|
|
9
10
|
description: "Create a new menu (saved meal plan) with the given name. Rejects duplicate names " +
|
|
10
11
|
"(case-insensitive exact match); if a duplicate is found, the response includes the existing UID. " +
|
|
11
12
|
"Optionally set the day span (default 1) and free-text notes.",
|
|
@@ -57,6 +58,7 @@ export function registerCreateMenuTool(server, ctx) {
|
|
|
57
58
|
export function registerUpdateMenuTool(server, ctx) {
|
|
58
59
|
const log = ctx.log.child({ component: "update_menu" });
|
|
59
60
|
server.registerTool("update_menu", {
|
|
61
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true },
|
|
60
62
|
description: "Update a menu's name, day span, and/or notes. Look it up by UID or name (tiered fuzzy match, " +
|
|
61
63
|
"case-insensitive). Provide at least one of name, days, or notes. Renaming to a name already used " +
|
|
62
64
|
"by a different menu is rejected (the existing UID is surfaced). Shrinking days below the highest " +
|
|
@@ -154,6 +156,7 @@ export function registerUpdateMenuTool(server, ctx) {
|
|
|
154
156
|
export function registerDeleteMenuTool(server, ctx) {
|
|
155
157
|
const log = ctx.log.child({ component: "delete_menu" });
|
|
156
158
|
server.registerTool("delete_menu", {
|
|
159
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false },
|
|
157
160
|
description: "Delete a menu and all of its planned recipes (menuitems). Look it up by UID or name (tiered fuzzy " +
|
|
158
161
|
"match, case-insensitive). The menuitems are tombstoned first, then the menu itself. " +
|
|
159
162
|
'Pass exactly one lookup shape: {"uid": "..."} or {"name": "..."}.',
|
|
@@ -20,6 +20,7 @@ const itemInputSchema = z.object({
|
|
|
20
20
|
export function registerAddPantryItemsTool(server, ctx) {
|
|
21
21
|
const log = ctx.log.child({ component: "add_pantry_items" });
|
|
22
22
|
server.registerTool("add_pantry_items", {
|
|
23
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
|
|
23
24
|
description: "Add one or more items to the pantry. Skips items that duplicate an existing ingredient (case-insensitive) " +
|
|
24
25
|
"and reports them with the existing UID and a suggestion to use update_pantry_item. " +
|
|
25
26
|
"All date fields are validated up-front; a single unparseable date rejects the entire batch.",
|
|
@@ -5,6 +5,7 @@ import { commitPantryItem, pantryStartGuard } from "./pantry-helpers.js";
|
|
|
5
5
|
export function registerDeletePantryItemTool(server, ctx) {
|
|
6
6
|
const log = ctx.log.child({ component: "delete_pantry_item" });
|
|
7
7
|
server.registerTool("delete_pantry_item", {
|
|
8
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true },
|
|
8
9
|
description: "Soft-delete a pantry item by UID. Idempotent: a second delete on the same UID " +
|
|
9
10
|
"returns a friendly 'already deleted' message without re-saving. Requires an exact UID.",
|
|
10
11
|
inputSchema: {
|
package/dist/tools/pantry-get.js
CHANGED
|
@@ -4,6 +4,7 @@ import { pantryItemToMarkdown, pantryStartGuard } from "./pantry-helpers.js";
|
|
|
4
4
|
export function registerGetPantryItemTool(server, ctx) {
|
|
5
5
|
const log = ctx.log.child({ component: "read_pantry_item" });
|
|
6
6
|
server.registerTool("read_pantry_item", {
|
|
7
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
7
8
|
description: "Get a pantry item by UID or ingredient name. Ingredient lookup is fuzzy " +
|
|
8
9
|
"(exact → starts-with → contains) and case-insensitive, with a disambiguation list " +
|
|
9
10
|
"when multiple items match the same tier. " +
|
|
@@ -3,6 +3,7 @@ import { pantryStartGuard } from "./pantry-helpers.js";
|
|
|
3
3
|
export function registerListPantryTool(server, ctx) {
|
|
4
4
|
const log = ctx.log.child({ component: "list_pantry_items" });
|
|
5
5
|
server.registerTool("list_pantry_items", {
|
|
6
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
6
7
|
description: "List all pantry items sorted alphabetically by ingredient name. Returns the ingredient, quantity, and aisle for each item. Use read_pantry_item with the UID for full details.",
|
|
7
8
|
inputSchema: {},
|
|
8
9
|
}, async () => {
|
|
@@ -16,6 +16,7 @@ export const restockPantryItemInputSchema = z
|
|
|
16
16
|
export function registerMarkPantryItemOutOfStockTool(server, ctx) {
|
|
17
17
|
const log = ctx.log.child({ component: "mark_pantry_item_out_of_stock" });
|
|
18
18
|
server.registerTool("mark_pantry_item_out_of_stock", {
|
|
19
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },
|
|
19
20
|
description: "Mark a pantry item as out of stock by UID (e.g. you've run out of it).",
|
|
20
21
|
inputSchema: markPantryItemOutOfStockInputSchema,
|
|
21
22
|
}, async (args) => {
|
|
@@ -43,6 +44,7 @@ export function registerMarkPantryItemOutOfStockTool(server, ctx) {
|
|
|
43
44
|
export function registerRestockPantryItemTool(server, ctx) {
|
|
44
45
|
const log = ctx.log.child({ component: "restock_pantry_item" });
|
|
45
46
|
server.registerTool("restock_pantry_item", {
|
|
47
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },
|
|
46
48
|
description: "Mark a pantry item as back in stock by UID (e.g. you've restocked it).",
|
|
47
49
|
inputSchema: restockPantryItemInputSchema,
|
|
48
50
|
}, async (args) => {
|
|
@@ -28,6 +28,7 @@ export const updatePantryItemInputSchema = z
|
|
|
28
28
|
export function registerUpdatePantryItemTool(server, ctx) {
|
|
29
29
|
const log = ctx.log.child({ component: "update_pantry_item" });
|
|
30
30
|
server.registerTool("update_pantry_item", {
|
|
31
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true },
|
|
31
32
|
description: "Update a pantry item's ingredient, quantity, aisle, or dates by UID. Only provided fields are " +
|
|
32
33
|
"changed; omitted fields retain their existing values. Setting expirationDate also updates " +
|
|
33
34
|
"hasExpiration accordingly. To change stock status, use mark_pantry_item_out_of_stock / restock_pantry_item.",
|
|
@@ -41,6 +41,7 @@ export const generatePhotoInputSchema = z.object({
|
|
|
41
41
|
export function registerGeneratePhotoTool(server, ctx, photographyClient) {
|
|
42
42
|
const log = ctx.log.child({ component: "generate_recipe_photo" });
|
|
43
43
|
server.registerTool("generate_recipe_photo", {
|
|
44
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
|
|
44
45
|
description: "Generate a styled food photo for a recipe with an AI image model and (by default) attach it to the " +
|
|
45
46
|
"recipe. The prompt is built from the recipe's name, description, and categories — so well-described, " +
|
|
46
47
|
"categorized recipes produce the best results; pass `style` to guide plating or describe an obscure dish. " +
|
|
@@ -99,6 +99,7 @@ async function resolveSourceBytes(source, ctx, recipeUid) {
|
|
|
99
99
|
export function registerUploadPhotoTool(server, ctx) {
|
|
100
100
|
const log = ctx.log.child({ component: "upload_recipe_photo" });
|
|
101
101
|
server.registerTool("upload_recipe_photo", {
|
|
102
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
|
|
102
103
|
description: "Attach a photo to a recipe from exactly one `source`: a `url` (PREFERRED for web images — the server " +
|
|
103
104
|
"downloads it), a `generation_token` (to save an image you previewed with generate_recipe_photo, attach:false — " +
|
|
104
105
|
"no need to regenerate), or, for programmatic callers, inline `image_base64`. If you built the recipe " +
|
|
@@ -160,6 +161,7 @@ export function registerUploadPhotoTool(server, ctx) {
|
|
|
160
161
|
export function registerDeletePhotoTool(server, ctx) {
|
|
161
162
|
const log = ctx.log.child({ component: "delete_recipe_photo" });
|
|
162
163
|
server.registerTool("delete_recipe_photo", {
|
|
164
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true },
|
|
163
165
|
description: "Delete a photo from a recipe by UID. Idempotent: a second delete on the same UID returns a friendly " +
|
|
164
166
|
"'already deleted' message without re-POSTing. Requires an exact photo UID.",
|
|
165
167
|
inputSchema: deletePhotoInputSchema.shape,
|
package/dist/tools/read.js
CHANGED
|
@@ -3,6 +3,7 @@ import { coldStartGuard, formatLookupOutcome, recipeToMarkdown, resolveLookup, u
|
|
|
3
3
|
export function registerReadTool(server, ctx) {
|
|
4
4
|
const log = ctx.log.child({ component: "read_recipe" });
|
|
5
5
|
server.registerTool("read_recipe", {
|
|
6
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
6
7
|
description: "Read a recipe by UID or title. Title lookup is fuzzy (exact → starts-with → contains) " +
|
|
7
8
|
"and returns a disambiguation list when multiple recipes match the same tier. " +
|
|
8
9
|
'Pass exactly one shape: {"uid": "..."} or {"title": "..."}.',
|
|
@@ -24,6 +24,7 @@ export const categorizeRecipeInputSchema = z
|
|
|
24
24
|
export function registerCategorizeRecipeTool(server, ctx) {
|
|
25
25
|
const log = ctx.log.child({ component: "categorize_recipe" });
|
|
26
26
|
server.registerTool("categorize_recipe", {
|
|
27
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true },
|
|
27
28
|
description: "Add, replace, or remove a recipe's categories by UID. Pass category names or UIDs and a mode: " +
|
|
28
29
|
"add (union with current — the default), replace (set exactly these), or remove (drop these). " +
|
|
29
30
|
"Unknown category names are skipped with a warning. To edit other recipe fields, use update_recipe.",
|
|
@@ -15,6 +15,7 @@ export const unfavoriteRecipeInputSchema = z
|
|
|
15
15
|
export function registerFavoriteRecipeTool(server, ctx) {
|
|
16
16
|
const log = ctx.log.child({ component: "favorite_recipe" });
|
|
17
17
|
server.registerTool("favorite_recipe", {
|
|
18
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },
|
|
18
19
|
description: "Mark a recipe as a favorite by UID (adds it to the Favorites list).",
|
|
19
20
|
inputSchema: favoriteRecipeInputSchema,
|
|
20
21
|
}, async (args) => {
|
|
@@ -43,6 +44,7 @@ export function registerFavoriteRecipeTool(server, ctx) {
|
|
|
43
44
|
export function registerUnfavoriteRecipeTool(server, ctx) {
|
|
44
45
|
const log = ctx.log.child({ component: "unfavorite_recipe" });
|
|
45
46
|
server.registerTool("unfavorite_recipe", {
|
|
47
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },
|
|
46
48
|
description: "Remove a recipe from the Favorites list by UID.",
|
|
47
49
|
inputSchema: unfavoriteRecipeInputSchema,
|
|
48
50
|
}, async (args) => {
|
|
@@ -11,6 +11,7 @@ export const rateRecipeInputSchema = z
|
|
|
11
11
|
export function registerRateRecipeTool(server, ctx) {
|
|
12
12
|
const log = ctx.log.child({ component: "rate_recipe" });
|
|
13
13
|
server.registerTool("rate_recipe", {
|
|
14
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },
|
|
14
15
|
description: "Rate a recipe 0–5 stars by UID. Sets the recipe's star rating; pass 0 to clear it.",
|
|
15
16
|
inputSchema: rateRecipeInputSchema,
|
|
16
17
|
}, async (args) => {
|
|
@@ -11,6 +11,7 @@ export const restoreRecipeInputSchema = z
|
|
|
11
11
|
export function registerRestoreRecipeTool(server, ctx) {
|
|
12
12
|
const log = ctx.log.child({ component: "restore_recipe" });
|
|
13
13
|
server.registerTool("restore_recipe", {
|
|
14
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },
|
|
14
15
|
description: "Restore a trashed recipe by UID, moving it out of the trash back into the active library. " +
|
|
15
16
|
"The inverse of trash_recipe; use purge_recipe to permanently delete a trashed recipe instead.",
|
|
16
17
|
inputSchema: restoreRecipeInputSchema,
|
package/dist/tools/search.js
CHANGED
|
@@ -31,6 +31,7 @@ export const searchRecipesInputSchema = z
|
|
|
31
31
|
export function registerSearchTool(server, ctx) {
|
|
32
32
|
const log = ctx.log.child({ component: "search_recipes" });
|
|
33
33
|
server.registerTool("search_recipes", {
|
|
34
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
34
35
|
description: "Search and filter recipes. Use any combination of: free-text query (matches name, " +
|
|
35
36
|
"ingredients, description), an ingredient list with all/any match mode, and/or max " +
|
|
36
37
|
"prep/cook/total time constraints. At least one criterion is required. Results are " +
|
package/dist/tools/update.js
CHANGED
|
@@ -29,6 +29,7 @@ export const updateRecipeInputSchema = z
|
|
|
29
29
|
export function registerUpdateTool(server, ctx) {
|
|
30
30
|
const log = ctx.log.child({ component: "update_recipe" });
|
|
31
31
|
server.registerTool("update_recipe", {
|
|
32
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true },
|
|
32
33
|
description: "Update a recipe's content fields by UID (name, ingredients, directions, description, notes, " +
|
|
33
34
|
"servings, prep/cook/total time, source, difficulty, nutritional info). Only provided fields " +
|
|
34
35
|
"change; omitted fields keep their values. This tool does NOT edit rating, categories, favorite " +
|