Skip to content

Commit 7221f04

Browse files
committed
fix(mcp): check the holes of a sparse content block array
Array.prototype.map skips the holes of a sparse array, so a hole passed the DF0081 check and reached the MCP client as null. Array.from visits each hole as undefined, so it now fails the check.
1 parent 4d326c3 commit 7221f04

2 files changed

Lines changed: 25 additions & 2 deletions

File tree

‎packages/agentic/src/mcp/__tests__/mcp-server.test.ts‎

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { StandardSchemaV1 } from '@standard-schema/spec'
2-
import type { DevframeHost } from 'devframe/types'
2+
import type { DevframeHost, McpContentBlock } from 'devframe/types'
33
import { Client, InMemoryTransport } from '@modelcontextprotocol/client'
44
import { withMcpContent } from 'devframe'
55
import { createHostContext } from 'devframe/node'
@@ -258,6 +258,28 @@ describe('mcp adapter (in-memory)', () => {
258258
}
259259
})
260260

261+
it('returns DF0081 when the attached content blocks have holes', async () => {
262+
const { ctx, client, cleanup } = await bootPair()
263+
try {
264+
// eslint-disable-next-line unicorn/no-new-array
265+
const sparse = new Array(2) as McpContentBlock[]
266+
sparse[1] = { type: 'text', text: 'second' }
267+
ctx.agent.registerTool({
268+
id: 'sparse',
269+
description: 'Returns a sparse block array.',
270+
handler: () => withMcpContent({}, sparse),
271+
})
272+
273+
const result = await client.callTool({ name: 'sparse', arguments: {} })
274+
expect(result.isError).toBe(true)
275+
const content = result.content as Array<{ type: string, text: string }>
276+
expect(content[0]!.text).toContain('DF0081')
277+
}
278+
finally {
279+
await cleanup()
280+
}
281+
})
282+
261283
it('calls an rpc-backed tool whose return schema has no native converter', async () => {
262284
const { ctx, client, cleanup } = await bootPair()
263285
try {

‎packages/agentic/src/mcp/build-server.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -379,7 +379,8 @@ function mcpContentOf(name: string, result: unknown): McpContentBlock[] | undefi
379379
return undefined
380380
if (!Array.isArray(blocks) || blocks.length === 0)
381381
throw diagnostics.DF0081({ name, reason: 'expected a non-empty array' })
382-
return blocks.map((block: unknown, index): McpContentBlock => {
382+
// `Array.from` visits the holes of a sparse array as `undefined`, so they fail the check below.
383+
return Array.from(blocks, (block: unknown, index): McpContentBlock => {
383384
const b = (block ?? {}) as Record<string, unknown>
384385
if (b.type === 'text' && typeof b.text === 'string')
385386
return { type: 'text', text: b.text }

0 commit comments

Comments
 (0)