From 2d08c1b07860563182a7b6d0e4c805eb83263e21 Mon Sep 17 00:00:00 2001 From: DLeibner Date: Tue, 22 Sep 2026 09:43:26 +0200 Subject: [PATCH] Return read_pdf_bytes payload as JSON text content in pdf-server read_pdf_bytes put the base64 bytes only in structuredContent; its text block was a size summary like "256 bytes at 0/641720". Clients that only render content[] (e.g. terminal MCP clients) could not read the data. Mirror the structured result as serialized JSON in the text block, as the MCP spec recommends for tools that return structured content. The viewer keeps reading structuredContent, so its behavior is unchanged. Fixes #762 --- examples/pdf-server/server.test.ts | 42 ++++++++++++++++++++++++++++++ examples/pdf-server/server.ts | 26 +++++++++--------- 2 files changed, 54 insertions(+), 14 deletions(-) diff --git a/examples/pdf-server/server.test.ts b/examples/pdf-server/server.test.ts index 0b2a5eb4d..24ef8c5fb 100644 --- a/examples/pdf-server/server.test.ts +++ b/examples/pdf-server/server.test.ts @@ -453,6 +453,48 @@ describe("display_pdf transport-error handling", () => { }); }); +describe("read_pdf_bytes result content", () => { + let tmpDir: string; + let tmpFile: string; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pdf-read-bytes-")); + tmpFile = path.join(tmpDir, "test.pdf"); + fs.writeFileSync(tmpFile, Buffer.from("%PDF-1.4\n%test\n")); + allowedLocalFiles.add(tmpFile); + }); + + afterEach(() => { + allowedLocalFiles.delete(tmpFile); + fs.rmSync(tmpDir, { recursive: true, force: true }); + }); + + it("mirrors structuredContent as JSON text for clients that only read content", async () => { + const server = createServer(); + const client = new Client({ name: "t", version: "1" }); + const [ct, st] = InMemoryTransport.createLinkedPair(); + await Promise.all([server.connect(st), client.connect(ct)]); + + try { + const result = await client.callTool({ + name: "read_pdf_bytes", + arguments: { url: tmpFile, offset: 0, byteCount: 8 }, + }); + expect(result.isError).toBeFalsy(); + + const content = result.content as { type: string; text: string }[]; + expect(content).toHaveLength(1); + expect(content[0].type).toBe("text"); + const parsed = JSON.parse(content[0].text); + expect(parsed).toEqual(result.structuredContent); + expect(Buffer.from(parsed.bytes, "base64").toString()).toBe("%PDF-1.4"); + } finally { + await client.close(); + await server.close(); + } + }); +}); + describe("extractFormSchema field-tree handling", () => { async function schemaFor(bytes: Uint8Array) { const doc = await getDocument({ data: bytes }).promise; diff --git a/examples/pdf-server/server.ts b/examples/pdf-server/server.ts index e67cd4731..edadadef0 100644 --- a/examples/pdf-server/server.ts +++ b/examples/pdf-server/server.ts @@ -1416,22 +1416,20 @@ export function createServer(options: CreateServerOptions = {}): McpServer { // Base64 encode for JSON transport const bytes = Buffer.from(data).toString("base64"); const hasMore = offset + data.length < totalBytes; + const chunk = { + url: normalized, + bytes, + offset, + byteCount: data.length, + totalBytes, + hasMore, + }; return { - content: [ - { - type: "text", - text: `${data.length} bytes at ${offset}/${totalBytes}`, - }, - ], - structuredContent: { - url: normalized, - bytes, - offset, - byteCount: data.length, - totalBytes, - hasMore, - }, + // Per the MCP spec, also return the serialized structured content + // as text so clients that only read content[] still get the bytes. + content: [{ type: "text", text: JSON.stringify(chunk) }], + structuredContent: chunk, }; } catch (err) { return {