mcp-marmiton 2.0.1 → 2.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/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.1.0
4
+
5
+ - **The largest page this reads is a setting, and pages past it are refused.**
6
+ A deadline abandons a body that arrives slowly; one that arrives quickly and
7
+ large is never abandoned by it, and it lands in memory in one piece before
8
+ anything looks at it. The body is read in pieces and stopped at
9
+ `MARMITON_MAX_BODY_BYTES`, eight megabytes by default.
10
+
3
11
  ## 2.0.1
4
12
 
5
13
  - **Every tool is documented, with its arguments and what its answer carries.**
package/PRIVACY.md CHANGED
@@ -55,11 +55,12 @@ becomes of them. `MARMITON_LOG_LEVEL` governs how much is written and defaults t
55
55
 
56
56
  ## The settings that change any of this
57
57
 
58
- | Variable | What it changes |
59
- | ----------------------- | ---------------------------------------------------------------------------------------------------------------- |
60
- | `MARMITON_USER_AGENT` | Adds your own identifier in front of this project's, which stays appended so the site can always reach a person. |
61
- | `MARMITON_CACHE_TTL_MS` | How long an answer is held in memory. `0` turns the cache off. |
62
- | `MARMITON_LOG_LEVEL` | How much is written to stderr. |
58
+ | Variable | What it changes |
59
+ | ------------------------- | ---------------------------------------------------------------------------------------------------------------- |
60
+ | `MARMITON_USER_AGENT` | Adds your own identifier in front of this project's, which stays appended so the site can always reach a person. |
61
+ | `MARMITON_CACHE_TTL_MS` | How long an answer is held in memory. `0` turns the cache off. |
62
+ | `MARMITON_MAX_BODY_BYTES` | The largest page this reads. A page past it is refused rather than held. |
63
+ | `MARMITON_LOG_LEVEL` | How much is written to stderr. |
63
64
 
64
65
  ## Children
65
66
 
@@ -131,11 +132,12 @@ qu'ils deviennent. `MARMITON_LOG_LEVEL` règle leur quantité et vaut `error` pa
131
132
 
132
133
  ## Les réglages qui changent tout cela
133
134
 
134
- | Variable | Ce qu'elle change |
135
- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------ |
136
- | `MARMITON_USER_AGENT` | Ajoute votre identifiant devant celui du projet, qui reste accolé pour que le site puisse toujours joindre une personne. |
137
- | `MARMITON_CACHE_TTL_MS` | Combien de temps une réponse est gardée en mémoire. `0` éteint le cache. |
138
- | `MARMITON_LOG_LEVEL` | La quantité écrite sur stderr. |
135
+ | Variable | Ce qu'elle change |
136
+ | ------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
137
+ | `MARMITON_USER_AGENT` | Ajoute votre identifiant devant celui du projet, qui reste accolé pour que le site puisse toujours joindre une personne. |
138
+ | `MARMITON_CACHE_TTL_MS` | Combien de temps une réponse est gardée en mémoire. `0` éteint le cache. |
139
+ | `MARMITON_MAX_BODY_BYTES` | La plus grande page lue. Au-delà, la page est refusée plutôt que gardée. |
140
+ | `MARMITON_LOG_LEVEL` | La quantité écrite sur stderr. |
139
141
 
140
142
  ## Les enfants
141
143
 
package/README.md CHANGED
@@ -6,6 +6,7 @@
6
6
  [![MCP Registry](https://img.shields.io/badge/MCP_Registry-listed-6E56CF)](https://registry.modelcontextprotocol.io/v0/servers?search=io.github.smeet666/mcp-marmiton)
7
7
  [![Glama](https://glama.ai/mcp/servers/smeet666/mcp-marmiton/badges/score.svg)](https://glama.ai/mcp/servers/smeet666/mcp-marmiton)
8
8
  [![M8ven](https://m8ven.ai/badge/mcp/smeet666-mcp-marmiton-1f9qvs?variant=verified)](https://m8ven.ai/mcp/smeet666-mcp-marmiton-1f9qvs)
9
+ [![LobeHub](https://lobehub.com/badge/mcp/smeet666-mcp-marmiton)](https://lobehub.com/mcp/smeet666-mcp-marmiton)
9
10
  [![Install in Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en/install-mcp?name=marmiton&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIm1jcC1tYXJtaXRvbiJdfQ%3D%3D)
10
11
  [![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=marmiton&config=%7B%22name%22%3A%22marmiton%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22mcp-marmiton%22%5D%7D)
11
12
 
@@ -62,7 +63,7 @@ Node 24 or later is required, and no environment variable has to be set.
62
63
  "mcpServers": {
63
64
  "marmiton": {
64
65
  "command": "docker",
65
- "args": ["run", "-i", "--rm", "ghcr.io/smeet666/mcp-marmiton:2.0.1"]
66
+ "args": ["run", "-i", "--rm", "ghcr.io/smeet666/mcp-marmiton:2.1.0"]
66
67
  }
67
68
  }
68
69
  }
@@ -74,7 +75,7 @@ because a TTY rewrites the stream. The container needs outbound HTTPS to
74
75
 
75
76
  ### Bundle, without npm
76
77
 
77
- Download `mcp-marmiton-2.0.1.mcpb` from
78
+ Download `mcp-marmiton-2.1.0.mcpb` from
78
79
  [the latest release](https://github.com/smeet666/mcp-marmiton/releases/latest)
79
80
  and open it. A client that supports MCP bundles installs it on its own, with no
80
81
  npm and no configuration file to edit. The bundle carries its dependencies, so
@@ -193,6 +194,7 @@ Every variable is optional. Set them in the `env` block of your client config.
193
194
  | `MARMITON_MAX_RETRIES` | `3` | Attempts after a transient failure, from 0 to 10. |
194
195
  | `MARMITON_CACHE_TTL_MS` | `900000` | How long a page stays in memory, from 0 to 86400000. |
195
196
  | `MARMITON_CACHE_MAX_ENTRIES` | `200` | Pages held in memory at once, from 0 to 10000. |
197
+ | `MARMITON_MAX_BODY_BYTES` | `8000000` | Largest page this reads, in bytes, from 100000 to 64000000. |
196
198
  | `MARMITON_LOG_LEVEL` | `error` | `silent`, `error`, `info` or `debug`, written to stderr. |
197
199
 
198
200
  A value outside its range falls back to the default, and the reason is written to
@@ -331,7 +333,7 @@ renseigner.
331
333
  "mcpServers": {
332
334
  "marmiton": {
333
335
  "command": "docker",
334
- "args": ["run", "-i", "--rm", "ghcr.io/smeet666/mcp-marmiton:2.0.1"]
336
+ "args": ["run", "-i", "--rm", "ghcr.io/smeet666/mcp-marmiton:2.1.0"]
335
337
  }
336
338
  }
337
339
  }
@@ -344,7 +346,7 @@ aucun identifiant.
344
346
 
345
347
  ### Bundle, sans npm
346
348
 
347
- Téléchargez `mcp-marmiton-2.0.1.mcpb` depuis
349
+ Téléchargez `mcp-marmiton-2.1.0.mcpb` depuis
348
350
  [la dernière publication](https://github.com/smeet666/mcp-marmiton/releases/latest)
349
351
  et ouvrez-le. Un client qui gère les bundles MCP l'installe seul, sans npm et
350
352
  sans fichier de configuration à modifier. Le bundle emporte ses dépendances, donc
@@ -466,6 +468,7 @@ configuration du client.
466
468
  | `MARMITON_MAX_RETRIES` | `3` | Tentatives après un échec passager, de 0 à 10. |
467
469
  | `MARMITON_CACHE_TTL_MS` | `900000` | Durée pendant laquelle une page reste en mémoire, de 0 à 86400000. |
468
470
  | `MARMITON_CACHE_MAX_ENTRIES` | `200` | Pages gardées en mémoire à la fois, de 0 à 10000. |
471
+ | `MARMITON_MAX_BODY_BYTES` | `8000000` | Plus grande page lue, en octets, de 100000 à 64000000. |
469
472
  | `MARMITON_LOG_LEVEL` | `error` | `silent`, `error`, `info` ou `debug`, écrit sur la sortie d'erreur. |
470
473
 
471
474
  Une valeur hors de sa plage retombe sur le défaut, et la raison est écrite sur la
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
8
8
  import process from "process";
9
9
 
10
10
  // src/version.ts
11
- var PKG_VERSION = "2.0.1";
11
+ var PKG_VERSION = "2.1.0";
12
12
  var REPO_URL = "https://github.com/smeet666/mcp-marmiton";
13
13
 
14
14
  // src/config.ts
@@ -19,6 +19,10 @@ var DEFAULTS = {
19
19
  maxRetries: 3,
20
20
  cacheTtlMs: 15 * 60 * 1e3,
21
21
  cacheMaxEntries: 200,
22
+ // Recipe pages run to tens of kilobytes. A body an order of magnitude past
23
+ // that is a page this reader has no reading for, and finding that out by
24
+ // holding it costs the caller the memory and the site the transfer.
25
+ maxBodyBytes: 8e6,
22
26
  logLevel: "error"
23
27
  };
24
28
  var MIN_ALLOWED_INTERVAL_MS = 500;
@@ -99,6 +103,11 @@ function loadConfig(env = process.env) {
99
103
  max: 1e4,
100
104
  fallback: DEFAULTS.cacheMaxEntries
101
105
  }),
106
+ maxBodyBytes: readNumber("MARMITON_MAX_BODY_BYTES", env, {
107
+ min: 1e5,
108
+ max: 64e6,
109
+ fallback: DEFAULTS.maxBodyBytes
110
+ }),
102
111
  logLevel
103
112
  };
104
113
  }
@@ -324,6 +333,36 @@ function readAnswer(url, status, body, retryAfterMs, ownGuessMs) {
324
333
  }
325
334
  return { kind: "usable" };
326
335
  }
336
+ async function readBounded(response, maxBytes, url) {
337
+ const stream = response.body;
338
+ if (!stream) {
339
+ return await response.text();
340
+ }
341
+ const reader = stream.getReader();
342
+ const decoder = new TextDecoder();
343
+ const parts = [];
344
+ let held = 0;
345
+ try {
346
+ for (; ; ) {
347
+ const { done, value } = await reader.read();
348
+ if (done) {
349
+ break;
350
+ }
351
+ held += value.byteLength;
352
+ if (held > maxBytes) {
353
+ throw parseFailure(
354
+ url,
355
+ `the page carries more than the ${maxBytes} bytes this reads for one page`
356
+ );
357
+ }
358
+ parts.push(decoder.decode(value, { stream: true }));
359
+ }
360
+ } finally {
361
+ await reader.cancel().catch(() => void 0);
362
+ }
363
+ parts.push(decoder.decode());
364
+ return parts.join("");
365
+ }
327
366
  async function fetchHtml(url, deps) {
328
367
  const { config, limiter, logger } = deps;
329
368
  const doFetch = deps.fetchImpl ?? fetch;
@@ -353,7 +392,7 @@ async function fetchHtml(url, deps) {
353
392
  });
354
393
  status = response.status;
355
394
  retryAfterMs = parseRetryAfter(response.headers.get("retry-after"));
356
- body = await response.text();
395
+ body = await readBounded(response, config.maxBodyBytes, url);
357
396
  } catch (error) {
358
397
  lastError = asTransportError(error, url);
359
398
  logger.debug(`${lastError.code} for ${url}: ${lastError.message}`);