agentdocs-mcp
Advanced tools
+31
-0
@@ -7,2 +7,33 @@ # Changelog | ||
| ## 0.10.1 — 2026-08-22 | ||
| Follow-ups from verifying 0.10.0 against production, and from watching an agent | ||
| actually try to use it. | ||
| ### Fixed | ||
| - **`get_page include_images` could return an HTML page as an image.** A deleted | ||
| or dangling `/api/uploads/` reference does not 404 — the app's catch-all answers | ||
| `200 text/html` — so `response.ok` was true and a 22 KB HTML page was base64'd | ||
| into the model's context under `mimeType: text/html`. The content type is now | ||
| checked: anything that isn't `image/*` is skipped and explained in | ||
| `image_notes`. This matters more since 0.10.0 shipped a delete endpoint, which | ||
| makes dangling references routine rather than rare. | ||
| - **`data` now accepts a `data:image/png;base64,...` URI**, not only bare base64. | ||
| Agents reach for the URI form naturally; previously the whole string was decoded, | ||
| produced garbage, and failed with a misleading "unsupported format" error. That | ||
| error message now also names the likely causes. | ||
| - **Dropped `absolute_url` from the upload result.** On the remote surface | ||
| `client.baseUrl` is a synthetic self-base that nothing dials, so the field | ||
| rendered as `http://127.0.0.1:3000/...` — a URL an agent would follow and fail | ||
| on. The relative `url` and `markdown` are what belong in a page. | ||
| ### Changed | ||
| - **`upload_image`'s description now explains how to supply an image**, because | ||
| field reports showed agents getting stuck here. It states the accepted formats | ||
| and size up front, describes each source and when it is usable, and names the | ||
| trap directly: an image you have only *viewed* (a screenshot another tool | ||
| returned) cannot be re-encoded from what you see — you saw pixels, not bytes, so | ||
| you need the actual file. The stdio and remote surfaces give different advice, | ||
| since `path` only exists on one of them. | ||
| ## 0.10.0 — 2026-08-22 | ||
@@ -9,0 +40,0 @@ |
+41
-13
@@ -27,3 +27,5 @@ import { z } from "zod"; | ||
| if (!hit) { | ||
| throw new Error("Unsupported image format. AgentDocs accepts PNG, JPEG, GIF and WebP (SVG is rejected server-side as a script-injection vector)."); | ||
| throw new Error("Unsupported image format: these bytes are not a PNG, JPEG, GIF or WebP. " + | ||
| "(SVG is rejected as a script-injection vector.) If you passed base64, check it is the encoding of the image FILE'S BYTES — " + | ||
| "text, a data: URI with a non-image type, or an HTML page will all land here."); | ||
| } | ||
@@ -87,11 +89,24 @@ return { mime: hit.mime, ext: hit.ext }; | ||
| // that will be refused. | ||
| // Written for an agent that HAS a screenshot and does not know how to hand it | ||
| // over. The failure this prevents is real: a model that merely *viewed* an | ||
| // image (a screenshot returned by another tool) cannot re-encode it — it saw | ||
| // pixels, not bytes — and will otherwise try to invent base64, or give up. | ||
| const sources = localFiles | ||
| ? 'Provide exactly one of "path" (a file on this machine), "source_url", or "data" (base64).' | ||
| : 'Provide exactly one of "source_url" or "data" (base64). "path" is unavailable on the remote server — it has no access to your filesystem.'; | ||
| ? 'HOW TO SUPPLY THE IMAGE — give exactly one of:\n' + | ||
| ' • path — absolute path to an image file on this machine. Prefer this: the bytes never pass through the conversation.\n' + | ||
| ' • data — base64 of the file\'s bytes (a "data:image/png;base64,..." URI is also accepted).\n' + | ||
| ' • source_url — a public http(s) URL; the server fetches it.\n' + | ||
| 'If you can only SEE an image (e.g. a screenshot another tool returned), you cannot re-encode it from what you see — you need the file. Save it to disk, then pass its "path".' | ||
| : 'HOW TO SUPPLY THE IMAGE — give exactly one of:\n' + | ||
| ' • data — base64 of the file\'s bytes (a "data:image/png;base64,..." URI is also accepted).\n' + | ||
| ' • source_url — a public http(s) URL; the server fetches it.\n' + | ||
| '"path" does NOT work here: this is a remote HTTP server with no access to your filesystem.\n' + | ||
| 'If you can only SEE an image (e.g. a screenshot another tool returned), you cannot re-encode it from what you see — you need the actual file bytes. If you have shell/file access, read the file and base64 it; otherwise host it somewhere reachable and use "source_url".'; | ||
| server.registerTool("upload_image", { | ||
| title: "Upload image", | ||
| description: "Attach an image (PNG, JPEG, GIF or WebP; max 5 MB) to a space and get back the Markdown to embed it in a page. " + | ||
| "Use this to include screenshots and diagrams in the pages you write, so whoever reads the page later — human or agent — can see what you saw. " + | ||
| description: "Attach an image to a space and get back the Markdown to embed it in a page. " + | ||
| "Use this to put screenshots and diagrams into the pages you write, so whoever reads the page later — human or agent — can see what you saw. " + | ||
| "ACCEPTS: PNG, JPEG, GIF, WebP. Max 5 MB. SVG is rejected (script-injection vector). The format is detected from the file's own bytes, not its name. " + | ||
| sources + | ||
| " Counts against the workspace's storage quota.", | ||
| " Counts against the workspace's image storage quota (Free 50 MB, Pro 5 GB).", | ||
| inputSchema: { | ||
@@ -106,6 +121,12 @@ space: z | ||
| .describe(localFiles | ||
| ? "Absolute path to an image file on this machine." | ||
| : "Not supported on the remote server; use source_url or data."), | ||
| source_url: z.string().optional().describe("Public http(s) URL to fetch the image from."), | ||
| data: z.string().optional().describe("Base64-encoded image bytes."), | ||
| ? "Absolute path to an image file on this machine. Best option — the bytes never enter the conversation." | ||
| : "NOT SUPPORTED on this remote server (it cannot read your filesystem). Use data or source_url."), | ||
| source_url: z | ||
| .string() | ||
| .optional() | ||
| .describe("Public http(s) URL the server fetches the image from. Must be publicly reachable — private/loopback addresses are refused."), | ||
| data: z | ||
| .string() | ||
| .optional() | ||
| .describe("Base64 of the image FILE'S BYTES (a \"data:image/png;base64,...\" URI is accepted too). Not a description of the image, and not something you can produce from an image you only viewed."), | ||
| filename: z.string().optional().describe("Original filename to record (cosmetic; defaults to image.<ext>)."), | ||
@@ -138,3 +159,8 @@ alt_text: z.string().optional().describe("Alt text for the returned Markdown snippet."), | ||
| else { | ||
| bytes = Buffer.from(data, "base64"); | ||
| // Strip a data: URI wrapper if present. Agents reach for | ||
| // "data:image/png;base64,AAA..." naturally, and feeding that whole | ||
| // string to Buffer.from would decode to garbage and then fail the | ||
| // format sniff with a misleading "unsupported format" error. | ||
| const payload = data.replace(/^data:[^;,]*;base64,/, "").trim(); | ||
| bytes = Buffer.from(payload, "base64"); | ||
| if (bytes.length === 0) | ||
@@ -150,6 +176,8 @@ throw new Error("data did not decode to any bytes — is it valid base64?"); | ||
| const result = await client.uploadFile(`/api/spaces/${spaceId}/uploads`, { bytes, filename: name, mimeType: mime }); | ||
| const absolute = `${client.baseUrl}${result.url}`; | ||
| // No absolute_url: on the remote surface client.baseUrl is a synthetic | ||
| // self-base that nothing dials (see AgentDocs' in-process dispatch), so | ||
| // it renders as http://127.0.0.1:3000/... — a URL an agent would follow | ||
| // and fail on. The relative url is what belongs in a page anyway. | ||
| return textResult({ | ||
| ...result, | ||
| absolute_url: absolute, | ||
| markdown: ``, | ||
@@ -156,0 +184,0 @@ next_step: "Paste the `markdown` value into a page (create_page / update_page / append_to_page) to display the image there.", |
+10
-0
@@ -36,2 +36,12 @@ import { z } from "zod"; | ||
| const { bytes, mimeType } = await client.fetchBinary(ref); | ||
| // A deleted or never-existent upload does NOT come back as a 404: the | ||
| // app's catch-all answers 200 with an HTML page, so response.ok is true | ||
| // and the HTML would sail through as "image" bytes — a 22 KB page | ||
| // base64'd into the model's context under mimeType text/html. Deleting | ||
| // an upload is a normal action now, so dangling references are routine. | ||
| // Trust the content type, not the status code. | ||
| if (!mimeType.startsWith("image/")) { | ||
| notes.push(`${ref} is not available (server returned ${mimeType || "an unknown type"} instead of an image) — it was probably deleted.`); | ||
| continue; | ||
| } | ||
| images.push({ type: "image", data: bytes.toString("base64"), mimeType }); | ||
@@ -38,0 +48,0 @@ } |
+2
-2
| { | ||
| "name": "agentdocs-mcp", | ||
| "mcpName": "io.github.hoornet/agentdocs-mcp", | ||
| "version": "0.10.0", | ||
| "description": "MCP server for AgentDocs (agentdocs.eu) \u2014 read, search, and write collaborative docs from any MCP client", | ||
| "version": "0.10.1", | ||
| "description": "MCP server for AgentDocs (agentdocs.eu) — read, search, and write collaborative docs from any MCP client", | ||
| "license": "MIT", | ||
@@ -7,0 +7,0 @@ "author": "Jure (https://agentdocs.eu)", |
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
95086
5.83%1291
3.03%