mealie-mcp 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Joshua
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,298 @@
1
+ # mealie-mcp
2
+
3
+ A [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server for
4
+ [Mealie](https://github.com/mealie-recipes/mealie), the self-hosted recipe
5
+ manager, meal planner and shopping-list app.
6
+
7
+ It exposes **every endpoint of the Mealie REST API** as an MCP tool, so an LLM
8
+ (Claude, etc.) can read and manage your recipes, meal plans, shopping lists,
9
+ cookbooks, households, users and more.
10
+
11
+ - 🧩 **Complete API coverage** — one tool per Mealie endpoint (~259 tools).
12
+ - 🔄 **Auto-adapts to your Mealie version** — on startup it fetches the OpenAPI
13
+ schema from *your* instance, so the tools always match exactly what your
14
+ server supports. A bundled snapshot is used as a fallback if the fetch fails.
15
+ - 🚀 **Zero install** — runs straight from `npx`, ideal for MCPHub, Claude
16
+ Desktop, Cursor, and any other MCP client.
17
+ - 🔒 **Safe by default options** — read-only mode and per-category include/exclude
18
+ filtering for clients that prefer fewer tools.
19
+
20
+ ---
21
+
22
+ ## Quick start
23
+
24
+ You need two things:
25
+
26
+ 1. The base URL of your Mealie instance, e.g. `https://mealie.example.com`
27
+ 2. A Mealie **API token** (see [Getting an API token](#getting-an-api-token))
28
+
29
+ Run it with `npx`:
30
+
31
+ ```bash
32
+ MEALIE_BASE_URL="https://mealie.example.com" \
33
+ MEALIE_API_TOKEN="your-long-lived-token" \
34
+ npx -y mealie-mcp
35
+ ```
36
+
37
+ The server speaks MCP over **stdio**, so you normally won't run it by hand —
38
+ your MCP client launches it for you using the config below.
39
+
40
+ ---
41
+
42
+ ## Client configuration
43
+
44
+ ### MCPHub / Claude Desktop / Cursor (generic MCP config)
45
+
46
+ Add an entry to your client's MCP servers config (for Claude Desktop this is
47
+ `claude_desktop_config.json`; MCPHub uses an equivalent `mcpServers` block):
48
+
49
+ ```json
50
+ {
51
+ "mcpServers": {
52
+ "mealie": {
53
+ "command": "npx",
54
+ "args": ["-y", "mealie-mcp"],
55
+ "env": {
56
+ "MEALIE_BASE_URL": "https://mealie.example.com",
57
+ "MEALIE_API_TOKEN": "your-long-lived-token"
58
+ }
59
+ }
60
+ }
61
+ }
62
+ ```
63
+
64
+ That's the only required configuration. Everything else is optional tuning.
65
+
66
+ ---
67
+
68
+ ## Getting an API token
69
+
70
+ In Mealie:
71
+
72
+ 1. Click your user avatar → **Manage Your Profile**.
73
+ 2. Open the **API Tokens** section (`/user/profile/api-tokens`).
74
+ 3. Create a token, give it a name, and copy it.
75
+
76
+ The token inherits the permissions of the user that created it, so create it
77
+ under a user/household with the access you want the LLM to have. To give the
78
+ model read-only-ish safety, also see [`MEALIE_READ_ONLY`](#configuration).
79
+
80
+ ---
81
+
82
+ ## Configuration
83
+
84
+ All configuration is via environment variables.
85
+
86
+ | Variable | Required | Default | Description |
87
+ | --- | --- | --- | --- |
88
+ | `MEALIE_BASE_URL` | ✅ | — | Base URL of your Mealie instance, e.g. `https://mealie.example.com`. |
89
+ | `MEALIE_API_TOKEN` | – | — | Long-lived Mealie API token (sent as `Authorization: Bearer`). Most endpoints need it. `MEALIE_TOKEN` is accepted as an alias. |
90
+ | `MEALIE_READ_ONLY` | – | `false` | When `true`, only expose `GET` endpoints. Great for a safe, read-only assistant. |
91
+ | `MEALIE_TOOLS` | – | — | Comma-separated **allow-list** of tool names or category prefixes to expose (e.g. `recipe,households_shopping_lists`). Empty = all. |
92
+ | `MEALIE_EXCLUDE_TOOLS` | – | — | Comma-separated **deny-list** of tool names or category prefixes to hide (e.g. `admin,groups_seeders`). |
93
+ | `MEALIE_USE_BUNDLED_SPEC` | – | `false` | Skip the live OpenAPI fetch and use the snapshot bundled with the package. |
94
+ | `MEALIE_OPENAPI_URL` | – | `${MEALIE_BASE_URL}/openapi.json` | Override where the OpenAPI schema is fetched from. |
95
+ | `MEALIE_TIMEOUT` | – | `60000` | Per-request timeout in milliseconds. |
96
+ | `MEALIE_ACCEPT_LANGUAGE` | – | — | Optional `Accept-Language` header forwarded to Mealie (affects e.g. ingredient parsing locale). |
97
+
98
+ ### Reducing the number of tools
99
+
100
+ Mealie has ~259 endpoints. Some MCP clients work better with fewer tools.
101
+ Use `MEALIE_TOOLS` / `MEALIE_EXCLUDE_TOOLS` with **category prefixes** (the part
102
+ before the verb in a tool name) to narrow things down. Examples:
103
+
104
+ ```bash
105
+ # Only recipes, meal plans and shopping lists:
106
+ MEALIE_TOOLS="recipe,households_mealplans,households_shopping"
107
+
108
+ # Everything except admin + group seeders:
109
+ MEALIE_EXCLUDE_TOOLS="admin,groups_seeders,groups_migrations"
110
+
111
+ # Read-only recipe browsing assistant:
112
+ MEALIE_READ_ONLY=true
113
+ MEALIE_TOOLS="recipe,explore"
114
+ ```
115
+
116
+ ---
117
+
118
+ ## How tools are named
119
+
120
+ Each tool is named `<category>_<operation>`, derived from the Mealie OpenAPI
121
+ tag and operation. For example:
122
+
123
+ | Tool | Method & path |
124
+ | --- | --- |
125
+ | `recipe_crud_get_all` | `GET /api/recipes` |
126
+ | `recipe_crud_get_one` | `GET /api/recipes/{slug}` |
127
+ | `recipe_crud_create_one` | `POST /api/recipes` |
128
+ | `households_shopping_lists_get_all` | `GET /api/households/shopping/lists` |
129
+ | `households_mealplans_create_one` | `POST /api/households/mealplans` |
130
+ | `app_about_get_app_info` | `GET /api/app/about` |
131
+
132
+ Each tool's input schema declares its path parameters, query parameters and
133
+ (where relevant) a `body` object — all generated directly from Mealie's OpenAPI
134
+ schema, so the model gets accurate, fully-typed arguments.
135
+
136
+ ### File uploads
137
+
138
+ Endpoints that upload files (recipe images, ZIP imports, backups, assets, …)
139
+ take their file fields as **absolute paths to local files**, which the server
140
+ reads and sends as multipart form data. The tool description tells the model
141
+ which fields are file paths.
142
+
143
+ ### Categories
144
+
145
+ <details>
146
+ <summary>All 57 categories</summary>
147
+
148
+ | Category | Tools |
149
+ | --- | --- |
150
+ | `admin_about` | 3 |
151
+ | `admin_ai_providers` | 4 |
152
+ | `admin_backups` | 6 |
153
+ | `admin_debug` | 1 |
154
+ | `admin_email` | 2 |
155
+ | `admin_maintenance` | 5 |
156
+ | `admin_manage_groups` | 5 |
157
+ | `admin_manage_households` | 5 |
158
+ | `admin_manage_users` | 7 |
159
+ | `app_about` | 3 |
160
+ | `explore_categories` | 2 |
161
+ | `explore_cookbooks` | 2 |
162
+ | `explore_foods` | 2 |
163
+ | `explore_households` | 2 |
164
+ | `explore_recipes` | 3 |
165
+ | `explore_tags` | 2 |
166
+ | `explore_tools` | 2 |
167
+ | `groups_ai_provider_settings` | 2 |
168
+ | `groups_ai_providers` | 4 |
169
+ | `groups_households` | 2 |
170
+ | `groups_migrations` | 1 |
171
+ | `groups_multi_purpose_labels` | 5 |
172
+ | `groups_reports` | 3 |
173
+ | `groups_seeders` | 3 |
174
+ | `groups_self_service` | 6 |
175
+ | `households_cookbooks` | 6 |
176
+ | `households_event_notifications` | 6 |
177
+ | `households_invitations` | 3 |
178
+ | `households_mealplan_rules` | 5 |
179
+ | `households_mealplans` | 7 |
180
+ | `households_recipe_actions` | 6 |
181
+ | `households_self_service` | 7 |
182
+ | `households_shopping_list_items` | 8 |
183
+ | `households_shopping_lists` | 9 |
184
+ | `households_webhooks` | 7 |
185
+ | `organizer_categories` | 7 |
186
+ | `organizer_tags` | 7 |
187
+ | `organizer_tools` | 6 |
188
+ | `recipe_bulk_actions` | 8 |
189
+ | `recipe_comments` | 6 |
190
+ | `recipe_crud` | 23 |
191
+ | `recipe_exports` | 2 |
192
+ | `recipe_images_and_assets` | 5 |
193
+ | `recipe_ingredient_parser` | 2 |
194
+ | `recipe_shared` | 2 |
195
+ | `recipe_timeline` | 6 |
196
+ | `recipes_foods` | 6 |
197
+ | `recipes_units` | 6 |
198
+ | `shared_recipes` | 4 |
199
+ | `users_authentication` | 5 |
200
+ | `users_crud` | 6 |
201
+ | `users_images` | 1 |
202
+ | `users_passwords` | 2 |
203
+ | `users_ratings` | 5 |
204
+ | `users_registration` | 1 |
205
+ | `users_tokens` | 2 |
206
+ | `utils` | 1 |
207
+
208
+ </details>
209
+
210
+ ---
211
+
212
+ ## Development
213
+
214
+ ```bash
215
+ git clone https://github.com/2fst4u/mealie-mcp.git
216
+ cd mealie-mcp
217
+ npm install
218
+
219
+ npm run build # compile TypeScript to dist/
220
+ npm test # run the test suite (node:test)
221
+ npm run typecheck # type-check without emitting
222
+
223
+ # Run from source against a Mealie instance:
224
+ MEALIE_BASE_URL="https://demo.mealie.io" npm run dev
225
+ ```
226
+
227
+ ### Updating the bundled OpenAPI snapshot
228
+
229
+ The server prefers the live schema from your own instance, but the bundled
230
+ snapshot (used as a fallback) can be refreshed from any Mealie instance:
231
+
232
+ ```bash
233
+ npm run refresh-spec -- https://demo.mealie.io
234
+ ```
235
+
236
+ ### Project layout
237
+
238
+ | Path | Purpose |
239
+ | --- | --- |
240
+ | `src/index.ts` | Entry point: load config + spec, start stdio server. |
241
+ | `src/config.ts` | Environment-variable configuration. |
242
+ | `src/openapi-loader.ts` | Fetch live OpenAPI schema with bundled fallback. |
243
+ | `src/tools.ts` | Generate one MCP tool per OpenAPI operation. |
244
+ | `src/schema.ts` | Build self-contained JSON Schemas (`$ref` → `$defs`). |
245
+ | `src/http-client.ts` | Execute requests (JSON / urlencoded / multipart / binary). |
246
+ | `src/server.ts` | MCP server wiring (`tools/list`, `tools/call`). |
247
+ | `openapi.snapshot.json` | Bundled fallback OpenAPI schema. |
248
+
249
+ ---
250
+
251
+ ## Releases & publishing
252
+
253
+ This repo ships two GitHub Actions workflows:
254
+
255
+ - **CI** (`.github/workflows/ci.yml`) — runs type-check, build and tests on every
256
+ pull request (and on non-`main` branch pushes) across Node 18/20/22.
257
+ - **Release** (`.github/workflows/release.yml`) — on every push/merge to `main`,
258
+ builds and tests, then creates a GitHub Release `v<version>` (with auto-generated
259
+ notes) and publishes to npm **if that version hasn't been released yet**. To cut
260
+ a release, bump `version` in `package.json` in your PR; merging it ships the release.
261
+
262
+ ### Publishing to npm (Trusted Publishing / OIDC)
263
+
264
+ The release workflow publishes via npm **Trusted Publishing**, so there is **no
265
+ `NPM_TOKEN` secret to store** — GitHub Actions authenticates to npm with a
266
+ short-lived OIDC token, and npm generates build provenance automatically.
267
+
268
+ npm requires a package to exist before you can attach a Trusted Publisher, so
269
+ there is a **one-time bootstrap** for the first ever publish:
270
+
271
+ 1. **Publish the first version manually** from your machine (this also claims the
272
+ package name):
273
+ ```bash
274
+ npm install # ensure deps
275
+ npm login # your normal npm account + 2FA
276
+ npm publish --access public
277
+ ```
278
+ 2. **Configure the Trusted Publisher** on npmjs.com: open the package page →
279
+ **Settings** → **Trusted Publisher** → **GitHub Actions**, and enter:
280
+ - **Organization or user:** `2fst4u`
281
+ - **Repository:** `mealie-mcp`
282
+ - **Workflow filename:** `release.yml`
283
+ - **Environment:** *(leave blank)*
284
+ 3. From then on, **every merge to `main` with a bumped version publishes
285
+ automatically** over OIDC — no tokens, no manual steps.
286
+
287
+ > Requirements (handled by the workflow): `id-token: write` permission, Node
288
+ > ≥ 22.14, and npm ≥ 11.5.1. The publish step safely skips if the version is
289
+ > already on npm, so re-runs and the bootstrap version won't cause failures.
290
+
291
+ ---
292
+
293
+ ## License
294
+
295
+ [MIT](./LICENSE)
296
+
297
+ This project is an independent client for Mealie and is not affiliated with the
298
+ Mealie project.
package/dist/config.js ADDED
@@ -0,0 +1,37 @@
1
+ // Runtime configuration, sourced entirely from environment variables so the
2
+ // server works cleanly as an `npx` stdio process launched by an MCP client.
3
+ function bool(value, fallback = false) {
4
+ if (value === undefined)
5
+ return fallback;
6
+ return /^(1|true|yes|on)$/i.test(value.trim());
7
+ }
8
+ function list(value) {
9
+ if (!value)
10
+ return [];
11
+ return value
12
+ .split(",")
13
+ .map((s) => s.trim())
14
+ .filter(Boolean);
15
+ }
16
+ export function loadConfig(env = process.env) {
17
+ const rawBase = env.MEALIE_BASE_URL?.trim();
18
+ if (!rawBase) {
19
+ throw new Error("MEALIE_BASE_URL is required (e.g. https://mealie.example.com). " +
20
+ "Set it in your MCP client config under the server's `env`.");
21
+ }
22
+ const baseUrl = rawBase.replace(/\/+$/, "");
23
+ const timeoutRaw = env.MEALIE_TIMEOUT?.trim();
24
+ const timeoutMs = timeoutRaw && /^\d+$/.test(timeoutRaw) ? Number(timeoutRaw) : 60_000;
25
+ return {
26
+ baseUrl,
27
+ token: env.MEALIE_API_TOKEN?.trim() || env.MEALIE_TOKEN?.trim() || undefined,
28
+ openapiUrl: env.MEALIE_OPENAPI_URL?.trim() || undefined,
29
+ useBundledSpec: bool(env.MEALIE_USE_BUNDLED_SPEC),
30
+ readOnly: bool(env.MEALIE_READ_ONLY),
31
+ include: list(env.MEALIE_TOOLS),
32
+ exclude: list(env.MEALIE_EXCLUDE_TOOLS),
33
+ timeoutMs,
34
+ acceptLanguage: env.MEALIE_ACCEPT_LANGUAGE?.trim() || undefined,
35
+ };
36
+ }
37
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,4EAA4E;AAuB5E,SAAS,IAAI,CAAC,KAAyB,EAAE,QAAQ,GAAG,KAAK;IACvD,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,QAAQ,CAAC;IACzC,OAAO,oBAAoB,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;AACjD,CAAC;AAED,SAAS,IAAI,CAAC,KAAyB;IACrC,IAAI,CAAC,KAAK;QAAE,OAAO,EAAE,CAAC;IACtB,OAAO,KAAK;SACT,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;SACpB,MAAM,CAAC,OAAO,CAAC,CAAC;AACrB,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,MAAyB,OAAO,CAAC,GAAG;IAC7D,MAAM,OAAO,GAAG,GAAG,CAAC,eAAe,EAAE,IAAI,EAAE,CAAC;IAC5C,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CACb,iEAAiE;YAC/D,4DAA4D,CAC/D,CAAC;IACJ,CAAC;IACD,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IAE5C,MAAM,UAAU,GAAG,GAAG,CAAC,cAAc,EAAE,IAAI,EAAE,CAAC;IAC9C,MAAM,SAAS,GAAG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;IAEvF,OAAO;QACL,OAAO;QACP,KAAK,EAAE,GAAG,CAAC,gBAAgB,EAAE,IAAI,EAAE,IAAI,GAAG,CAAC,YAAY,EAAE,IAAI,EAAE,IAAI,SAAS;QAC5E,UAAU,EAAE,GAAG,CAAC,kBAAkB,EAAE,IAAI,EAAE,IAAI,SAAS;QACvD,cAAc,EAAE,IAAI,CAAC,GAAG,CAAC,uBAAuB,CAAC;QACjD,QAAQ,EAAE,IAAI,CAAC,GAAG,CAAC,gBAAgB,CAAC;QACpC,OAAO,EAAE,IAAI,CAAC,GAAG,CAAC,YAAY,CAAC;QAC/B,OAAO,EAAE,IAAI,CAAC,GAAG,CAAC,oBAAoB,CAAC;QACvC,SAAS;QACT,cAAc,EAAE,GAAG,CAAC,sBAAsB,EAAE,IAAI,EAAE,IAAI,SAAS;KAChE,CAAC;AACJ,CAAC"}
@@ -0,0 +1,158 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { basename } from "node:path";
3
+ const MAX_TEXT = 100_000;
4
+ function text(value) {
5
+ return { type: "text", text: value };
6
+ }
7
+ function truncate(value) {
8
+ if (value.length <= MAX_TEXT)
9
+ return value;
10
+ return `${value.slice(0, MAX_TEXT)}\n\n…[truncated ${value.length - MAX_TEXT} characters]`;
11
+ }
12
+ function buildUrl(config, tool, args) {
13
+ let path = tool.path;
14
+ for (const name of tool.pathParams) {
15
+ const value = args[name];
16
+ if (value === undefined || value === null) {
17
+ throw new Error(`Missing required path parameter: ${name}`);
18
+ }
19
+ path = path.replace(`{${name}}`, encodeURIComponent(String(value)));
20
+ }
21
+ const url = new URL(config.baseUrl + path);
22
+ for (const { name, isArray } of tool.queryParams) {
23
+ const value = args[name];
24
+ if (value === undefined || value === null)
25
+ continue;
26
+ if (isArray && Array.isArray(value)) {
27
+ for (const item of value)
28
+ url.searchParams.append(name, scalar(item));
29
+ }
30
+ else if (Array.isArray(value)) {
31
+ for (const item of value)
32
+ url.searchParams.append(name, scalar(item));
33
+ }
34
+ else {
35
+ url.searchParams.append(name, scalar(value));
36
+ }
37
+ }
38
+ return url.toString();
39
+ }
40
+ function scalar(value) {
41
+ if (typeof value === "string")
42
+ return value;
43
+ if (typeof value === "object")
44
+ return JSON.stringify(value);
45
+ return String(value);
46
+ }
47
+ async function buildMultipart(tool, body) {
48
+ const form = new FormData();
49
+ const fileFields = new Set(tool.body?.fileFields ?? []);
50
+ for (const [key, value] of Object.entries(body)) {
51
+ if (value === undefined || value === null)
52
+ continue;
53
+ if (fileFields.has(key)) {
54
+ const paths = Array.isArray(value) ? value : [value];
55
+ for (const p of paths) {
56
+ const filePath = String(p);
57
+ const data = await readFile(filePath);
58
+ form.append(key, new Blob([new Uint8Array(data)]), basename(filePath));
59
+ }
60
+ }
61
+ else if (Array.isArray(value)) {
62
+ for (const item of value)
63
+ form.append(key, scalar(item));
64
+ }
65
+ else {
66
+ form.append(key, scalar(value));
67
+ }
68
+ }
69
+ return form;
70
+ }
71
+ async function readBody(res) {
72
+ const contentType = res.headers.get("content-type") ?? "";
73
+ if (res.status === 204 || res.headers.get("content-length") === "0") {
74
+ return { blocks: [text(`Success (HTTP ${res.status}, no content).`)], raw: "" };
75
+ }
76
+ if (contentType.startsWith("image/")) {
77
+ const buf = Buffer.from(await res.arrayBuffer());
78
+ return {
79
+ blocks: [{ type: "image", data: buf.toString("base64"), mimeType: contentType.split(";")[0] }],
80
+ raw: `[image ${contentType} ${buf.length} bytes]`,
81
+ };
82
+ }
83
+ if (contentType.includes("application/json")) {
84
+ const raw = await res.text();
85
+ try {
86
+ const pretty = JSON.stringify(JSON.parse(raw), null, 2);
87
+ return { blocks: [text(truncate(pretty))], raw };
88
+ }
89
+ catch {
90
+ return { blocks: [text(truncate(raw))], raw };
91
+ }
92
+ }
93
+ if (contentType.startsWith("text/") || contentType.includes("xml") || contentType.includes("yaml")) {
94
+ const raw = await res.text();
95
+ return { blocks: [text(truncate(raw))], raw };
96
+ }
97
+ // Other binary payloads (zip, pdf, octet-stream): summarize instead of dumping base64.
98
+ const buf = Buffer.from(await res.arrayBuffer());
99
+ return {
100
+ blocks: [text(`Received ${buf.length} bytes of binary data (${contentType || "unknown type"}).`)],
101
+ raw: `[binary ${buf.length} bytes]`,
102
+ };
103
+ }
104
+ export async function executeTool(config, tool, args) {
105
+ const url = buildUrl(config, tool, args);
106
+ const headers = { Accept: "application/json" };
107
+ if (config.token)
108
+ headers.Authorization = `Bearer ${config.token}`;
109
+ if (config.acceptLanguage)
110
+ headers["Accept-Language"] = config.acceptLanguage;
111
+ let payload;
112
+ if (tool.body && args.body !== undefined && args.body !== null) {
113
+ const bodyValue = args.body;
114
+ if (tool.body.kind === "json") {
115
+ payload = JSON.stringify(bodyValue);
116
+ headers["Content-Type"] = "application/json";
117
+ }
118
+ else if (tool.body.kind === "urlencoded") {
119
+ const params = new URLSearchParams();
120
+ for (const [k, v] of Object.entries(bodyValue)) {
121
+ if (v !== undefined && v !== null)
122
+ params.append(k, scalar(v));
123
+ }
124
+ payload = params;
125
+ }
126
+ else {
127
+ payload = await buildMultipart(tool, bodyValue);
128
+ }
129
+ }
130
+ const controller = new AbortController();
131
+ const timer = setTimeout(() => controller.abort(), config.timeoutMs);
132
+ let res;
133
+ try {
134
+ res = await fetch(url, {
135
+ method: tool.method.toUpperCase(),
136
+ headers,
137
+ body: payload,
138
+ signal: controller.signal,
139
+ });
140
+ }
141
+ catch (err) {
142
+ const reason = err instanceof Error ? err.message : String(err);
143
+ return { content: [text(`Request to ${tool.method.toUpperCase()} ${url} failed: ${reason}`)], isError: true };
144
+ }
145
+ finally {
146
+ clearTimeout(timer);
147
+ }
148
+ const { blocks } = await readBody(res);
149
+ if (!res.ok) {
150
+ const detail = blocks.map((b) => (b.type === "text" ? b.text : "[binary]")).join("\n");
151
+ return {
152
+ content: [text(`HTTP ${res.status} ${res.statusText} from ${tool.method.toUpperCase()} ${tool.path}\n${detail}`)],
153
+ isError: true,
154
+ };
155
+ }
156
+ return { content: blocks };
157
+ }
158
+ //# sourceMappingURL=http-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http-client.js","sourceRoot":"","sources":["../src/http-client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAcrC,MAAM,QAAQ,GAAG,OAAO,CAAC;AAEzB,SAAS,IAAI,CAAC,KAAa;IACzB,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;AACvC,CAAC;AAED,SAAS,QAAQ,CAAC,KAAa;IAC7B,IAAI,KAAK,CAAC,MAAM,IAAI,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC3C,OAAO,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,mBAAmB,KAAK,CAAC,MAAM,GAAG,QAAQ,cAAc,CAAC;AAC7F,CAAC;AAED,SAAS,QAAQ,CAAC,MAAc,EAAE,IAAgB,EAAE,IAA6B;IAC/E,IAAI,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;IACrB,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;QACnC,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC;QACzB,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;YAC1C,MAAM,IAAI,KAAK,CAAC,oCAAoC,IAAI,EAAE,CAAC,CAAC;QAC9D,CAAC;QACD,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,IAAI,GAAG,EAAE,kBAAkB,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACtE,CAAC;IAED,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC;IAC3C,KAAK,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;QACjD,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC;QACzB,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI;YAAE,SAAS;QACpD,IAAI,OAAO,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YACpC,KAAK,MAAM,IAAI,IAAI,KAAK;gBAAE,GAAG,CAAC,YAAY,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;QACxE,CAAC;aAAM,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YAChC,KAAK,MAAM,IAAI,IAAI,KAAK;gBAAE,GAAG,CAAC,YAAY,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;QACxE,CAAC;aAAM,CAAC;YACN,GAAG,CAAC,YAAY,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;QAC/C,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC,QAAQ,EAAE,CAAC;AACxB,CAAC;AAED,SAAS,MAAM,CAAC,KAAc;IAC5B,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC5C,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IAC5D,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;AACvB,CAAC;AAED,KAAK,UAAU,cAAc,CAAC,IAAgB,EAAE,IAA6B;IAC3E,MAAM,IAAI,GAAG,IAAI,QAAQ,EAAE,CAAC;IAC5B,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,UAAU,IAAI,EAAE,CAAC,CAAC;IACxD,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QAChD,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI;YAAE,SAAS;QACpD,IAAI,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YACxB,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;YACrD,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;gBACtB,MAAM,QAAQ,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;gBAC3B,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,QAAQ,CAAC,CAAC;gBACtC,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,IAAI,IAAI,CAAC,CAAC,IAAI,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC;YACzE,CAAC;QACH,CAAC;aAAM,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YAChC,KAAK,MAAM,IAAI,IAAI,KAAK;gBAAE,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;QAC3D,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;QAClC,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,KAAK,UAAU,QAAQ,CAAC,GAAa;IACnC,MAAM,WAAW,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,EAAE,CAAC;IAE1D,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,KAAK,GAAG,EAAE,CAAC;QACpE,OAAO,EAAE,MAAM,EAAE,CAAC,IAAI,CAAC,iBAAiB,GAAG,CAAC,MAAM,gBAAgB,CAAC,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE,CAAC;IAClF,CAAC;IAED,IAAI,WAAW,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QACrC,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,WAAW,EAAE,CAAC,CAAC;QACjD,OAAO;YACL,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,EAAE,WAAW,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;YAC9F,GAAG,EAAE,UAAU,WAAW,IAAI,GAAG,CAAC,MAAM,SAAS;SAClD,CAAC;IACJ,CAAC;IAED,IAAI,WAAW,CAAC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,CAAC;QAC7C,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;QAC7B,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;YACxD,OAAO,EAAE,MAAM,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC;QACnD,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,EAAE,MAAM,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC;QAChD,CAAC;IACH,CAAC;IAED,IAAI,WAAW,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,WAAW,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,WAAW,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;QACnG,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;QAC7B,OAAO,EAAE,MAAM,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC;IAChD,CAAC;IAED,uFAAuF;IACvF,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,WAAW,EAAE,CAAC,CAAC;IACjD,OAAO;QACL,MAAM,EAAE,CAAC,IAAI,CAAC,YAAY,GAAG,CAAC,MAAM,0BAA0B,WAAW,IAAI,cAAc,IAAI,CAAC,CAAC;QACjG,GAAG,EAAE,WAAW,GAAG,CAAC,MAAM,SAAS;KACpC,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,MAAc,EAAE,IAAgB,EAAE,IAA6B;IAC/F,MAAM,GAAG,GAAG,QAAQ,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;IAEzC,MAAM,OAAO,GAA2B,EAAE,MAAM,EAAE,kBAAkB,EAAE,CAAC;IACvE,IAAI,MAAM,CAAC,KAAK;QAAE,OAAO,CAAC,aAAa,GAAG,UAAU,MAAM,CAAC,KAAK,EAAE,CAAC;IACnE,IAAI,MAAM,CAAC,cAAc;QAAE,OAAO,CAAC,iBAAiB,CAAC,GAAG,MAAM,CAAC,cAAc,CAAC;IAE9E,IAAI,OAAwD,CAAC;IAC7D,IAAI,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,IAAI,IAAI,CAAC,IAAI,KAAK,IAAI,EAAE,CAAC;QAC/D,MAAM,SAAS,GAAG,IAAI,CAAC,IAA+B,CAAC;QACvD,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;YAC9B,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;YACpC,OAAO,CAAC,cAAc,CAAC,GAAG,kBAAkB,CAAC;QAC/C,CAAC;aAAM,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;YAC3C,MAAM,MAAM,GAAG,IAAI,eAAe,EAAE,CAAC;YACrC,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC;gBAC/C,IAAI,CAAC,KAAK,SAAS,IAAI,CAAC,KAAK,IAAI;oBAAE,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;YACjE,CAAC;YACD,OAAO,GAAG,MAAM,CAAC;QACnB,CAAC;aAAM,CAAC;YACN,OAAO,GAAG,MAAM,cAAc,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;QAClD,CAAC;IACH,CAAC;IAED,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;IACzC,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC;IACrE,IAAI,GAAa,CAAC;IAClB,IAAI,CAAC;QACH,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;YACrB,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE;YACjC,OAAO;YACP,IAAI,EAAE,OAAO;YACb,MAAM,EAAE,UAAU,CAAC,MAAM;SAC1B,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,MAAM,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAChE,OAAO,EAAE,OAAO,EAAE,CAAC,IAAI,CAAC,cAAc,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,IAAI,GAAG,YAAY,MAAM,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IAChH,CAAC;YAAS,CAAC;QACT,YAAY,CAAC,KAAK,CAAC,CAAC;IACtB,CAAC;IAED,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,QAAQ,CAAC,GAAG,CAAC,CAAC;IAEvC,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;QACZ,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACvF,OAAO;YACL,OAAO,EAAE,CAAC,IAAI,CAAC,QAAQ,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,UAAU,SAAS,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC,CAAC;YACjH,OAAO,EAAE,IAAI;SACd,CAAC;IACJ,CAAC;IAED,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC;AAC7B,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env node
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { loadConfig } from "./config.js";
4
+ import { loadOpenApi } from "./openapi-loader.js";
5
+ import { filterTools, generateTools } from "./tools.js";
6
+ import { createServer, SERVER_NAME, SERVER_VERSION } from "./server.js";
7
+ function log(message) {
8
+ process.stderr.write(`[mealie-mcp] ${message}\n`);
9
+ }
10
+ async function main() {
11
+ const config = loadConfig();
12
+ const { doc, source } = await loadOpenApi(config);
13
+ const allTools = generateTools(doc);
14
+ const tools = filterTools(allTools, config);
15
+ if (tools.length === 0) {
16
+ log("Warning: no tools matched your include/exclude filters. The server will expose nothing.");
17
+ }
18
+ const categories = new Set(tools.map((t) => t.category));
19
+ log(`${SERVER_NAME} v${SERVER_VERSION}`);
20
+ log(`Mealie: ${config.baseUrl} | spec: ${source} (${doc.info?.version ?? "unknown"} version)`);
21
+ log(`Exposing ${tools.length}/${allTools.length} tools across ${categories.size} categories.`);
22
+ if (!config.token) {
23
+ log("No MEALIE_API_TOKEN set — only unauthenticated endpoints will succeed.");
24
+ }
25
+ const server = createServer(config, tools);
26
+ const transport = new StdioServerTransport();
27
+ await server.connect(transport);
28
+ log("Server ready on stdio.");
29
+ }
30
+ main().catch((err) => {
31
+ const reason = err instanceof Error ? err.stack || err.message : String(err);
32
+ process.stderr.write(`[mealie-mcp] Fatal: ${reason}\n`);
33
+ process.exit(1);
34
+ });
35
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AACxD,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAExE,SAAS,GAAG,CAAC,OAAe;IAC1B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,gBAAgB,OAAO,IAAI,CAAC,CAAC;AACpD,CAAC;AAED,KAAK,UAAU,IAAI;IACjB,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;IAE5B,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,WAAW,CAAC,MAAM,CAAC,CAAC;IAClD,MAAM,QAAQ,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC;IACpC,MAAM,KAAK,GAAG,WAAW,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IAE5C,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,GAAG,CAAC,yFAAyF,CAAC,CAAC;IACjG,CAAC;IAED,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC;IACzD,GAAG,CAAC,GAAG,WAAW,KAAK,cAAc,EAAE,CAAC,CAAC;IACzC,GAAG,CAAC,WAAW,MAAM,CAAC,OAAO,YAAY,MAAM,KAAK,GAAG,CAAC,IAAI,EAAE,OAAO,IAAI,SAAS,WAAW,CAAC,CAAC;IAC/F,GAAG,CAAC,YAAY,KAAK,CAAC,MAAM,IAAI,QAAQ,CAAC,MAAM,iBAAiB,UAAU,CAAC,IAAI,cAAc,CAAC,CAAC;IAC/F,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;QAClB,GAAG,CAAC,wEAAwE,CAAC,CAAC;IAChF,CAAC;IAED,MAAM,MAAM,GAAG,YAAY,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IAC3C,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAChC,GAAG,CAAC,wBAAwB,CAAC,CAAC;AAChC,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACnB,MAAM,MAAM,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAC7E,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,uBAAuB,MAAM,IAAI,CAAC,CAAC;IACxD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
@@ -0,0 +1,55 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { fileURLToPath } from "node:url";
3
+ import { dirname, join } from "node:path";
4
+ const here = dirname(fileURLToPath(import.meta.url));
5
+ // Bundled snapshot lives at the package root (one level above src/ or dist/).
6
+ const SNAPSHOT_PATH = join(here, "..", "openapi.snapshot.json");
7
+ async function loadBundled() {
8
+ const raw = await readFile(SNAPSHOT_PATH, "utf8");
9
+ return JSON.parse(raw);
10
+ }
11
+ async function fetchLive(url, timeoutMs) {
12
+ const controller = new AbortController();
13
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
14
+ try {
15
+ const res = await fetch(url, {
16
+ signal: controller.signal,
17
+ headers: { Accept: "application/json" },
18
+ });
19
+ if (!res.ok) {
20
+ throw new Error(`HTTP ${res.status} ${res.statusText}`);
21
+ }
22
+ return (await res.json());
23
+ }
24
+ finally {
25
+ clearTimeout(timer);
26
+ }
27
+ }
28
+ /**
29
+ * Load the OpenAPI document that drives tool generation.
30
+ *
31
+ * Preference order:
32
+ * 1. If `useBundledSpec` is set, use the snapshot shipped with the package.
33
+ * 2. Otherwise fetch `${baseUrl}/openapi.json` (or `openapiUrl`) so tools match
34
+ * the exact Mealie version the user runs.
35
+ * 3. On any failure, fall back to the bundled snapshot.
36
+ */
37
+ export async function loadOpenApi(config) {
38
+ if (config.useBundledSpec) {
39
+ return { doc: await loadBundled(), source: "bundled" };
40
+ }
41
+ const url = config.openapiUrl ?? `${config.baseUrl}/openapi.json`;
42
+ try {
43
+ const doc = await fetchLive(url, config.timeoutMs);
44
+ if (!doc?.paths || typeof doc.paths !== "object") {
45
+ throw new Error("response did not contain an OpenAPI `paths` object");
46
+ }
47
+ return { doc, source: "live" };
48
+ }
49
+ catch (err) {
50
+ const reason = err instanceof Error ? err.message : String(err);
51
+ process.stderr.write(`[mealie-mcp] Could not fetch live spec from ${url} (${reason}); falling back to bundled snapshot.\n`);
52
+ return { doc: await loadBundled(), source: "bundled" };
53
+ }
54
+ }
55
+ //# sourceMappingURL=openapi-loader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openapi-loader.js","sourceRoot":"","sources":["../src/openapi-loader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAS1C,MAAM,IAAI,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AACrD,8EAA8E;AAC9E,MAAM,aAAa,GAAG,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,uBAAuB,CAAC,CAAC;AAEhE,KAAK,UAAU,WAAW;IACxB,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC;IAClD,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAoB,CAAC;AAC5C,CAAC;AAED,KAAK,UAAU,SAAS,CAAC,GAAW,EAAE,SAAiB;IACrD,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;IACzC,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,SAAS,CAAC,CAAC;IAC9D,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;YAC3B,MAAM,EAAE,UAAU,CAAC,MAAM;YACzB,OAAO,EAAE,EAAE,MAAM,EAAE,kBAAkB,EAAE;SACxC,CAAC,CAAC;QACH,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,MAAM,IAAI,KAAK,CAAC,QAAQ,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,UAAU,EAAE,CAAC,CAAC;QAC1D,CAAC;QACD,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAoB,CAAC;IAC/C,CAAC;YAAS,CAAC;QACT,YAAY,CAAC,KAAK,CAAC,CAAC;IACtB,CAAC;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,MAAc;IAC9C,IAAI,MAAM,CAAC,cAAc,EAAE,CAAC;QAC1B,OAAO,EAAE,GAAG,EAAE,MAAM,WAAW,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;IACzD,CAAC;IAED,MAAM,GAAG,GAAG,MAAM,CAAC,UAAU,IAAI,GAAG,MAAM,CAAC,OAAO,eAAe,CAAC;IAClE,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,SAAS,CAAC,GAAG,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC;QACnD,IAAI,CAAC,GAAG,EAAE,KAAK,IAAI,OAAO,GAAG,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;YACjD,MAAM,IAAI,KAAK,CAAC,oDAAoD,CAAC,CAAC;QACxE,CAAC;QACD,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;IACjC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,MAAM,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAChE,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,+CAA+C,GAAG,KAAK,MAAM,wCAAwC,CACtG,CAAC;QACF,OAAO,EAAE,GAAG,EAAE,MAAM,WAAW,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;IACzD,CAAC;AACH,CAAC"}
@@ -0,0 +1,3 @@
1
+ // Minimal structural types for the subset of OpenAPI 3.1 that Mealie emits.
2
+ export const HTTP_METHODS = ["get", "post", "put", "patch", "delete"];
3
+ //# sourceMappingURL=openapi-types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openapi-types.js","sourceRoot":"","sources":["../src/openapi-types.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAiD5E,MAAM,CAAC,MAAM,YAAY,GAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC"}