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 +21 -0
- package/README.md +298 -0
- package/dist/config.js +37 -0
- package/dist/config.js.map +1 -0
- package/dist/http-client.js +158 -0
- package/dist/http-client.js.map +1 -0
- package/dist/index.js +35 -0
- package/dist/index.js.map +1 -0
- package/dist/openapi-loader.js +55 -0
- package/dist/openapi-loader.js.map +1 -0
- package/dist/openapi-types.js +3 -0
- package/dist/openapi-types.js.map +1 -0
- package/dist/schema.js +84 -0
- package/dist/schema.js.map +1 -0
- package/dist/server.js +39 -0
- package/dist/server.js.map +1 -0
- package/dist/tools.js +221 -0
- package/dist/tools.js.map +1 -0
- package/openapi.snapshot.json +1 -0
- package/package.json +57 -0
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":"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"}
|
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 @@
|
|
|
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"}
|