fix(mcp): project image results through durable attachments

This commit is contained in:
Tianyi Cui
2026-08-11 15:36:03 +08:00
parent e00146be73
commit 49426cae02
12 changed files with 787 additions and 81 deletions

View File

@@ -46,7 +46,7 @@ server.registerTool('image', {
}, async () => ({
content: [
{ type: 'text', text: 'Here is an image:' },
{ type: 'image', data: 'iVBORw0KGgo=', mimeType: 'image/png' },
{ type: 'image', data: 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC', mimeType: 'image/png' },
{ type: 'text', text: 'End of image.' },
],
}))

View File

@@ -19,9 +19,11 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'
import { z } from 'zod'
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'
import LocalAttachmentStore from '@deepseek-ai/dsh-attachment-local'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry from '@deepseek-ai/dsh-tools'
import { CallId } from '@deepseek-ai/dsh-llm'
import { CallId, LlmAdapter, LlmService } from '@deepseek-ai/dsh-llm'
import type { GenerateOptions, LlmResolvedModelInfo, StreamChunk } from '@deepseek-ai/dsh-llm'
import { apply } from '@deepseek-ai/dsh-mcp-client/src/index.ts'
import { publicToolName } from '@deepseek-ai/dsh-mcp-client/src/tools.ts'
import type { Config } from '@deepseek-ai/dsh-mcp-client'
@@ -43,6 +45,33 @@ async function mountRegistry(): Promise<Context> {
return ctx
}
/** Exact-route adapter used to prove real MCP image admission without an API key. */
class ImageAdapter extends LlmAdapter {
override resolveModel(provider: string, model: string): Promise<LlmResolvedModelInfo> {
return Promise.resolve({ provider, id: model, name: model, inputModalities: ['text', 'image'] })
}
stream(_options: GenerateOptions): AsyncIterable<StreamChunk> {
throw new Error('MCP image e2e never streams')
}
}
async function mountImageRegistry(dshHome: string): Promise<Context> {
const ctx = await mountRegistry()
await ctx.plugin(LocalAttachmentStore, { dshHome })
await ctx.plugin(LlmService)
ctx.llm.registerAdapter(['visual'], new ImageAdapter())
return ctx
}
/** Calling-agent stand-in pinned to the keyless image-capable route. */
function imageAgent(): object {
return {
options: { provider: 'visual', model: 'vision' },
session: { requestHeader: () => undefined },
}
}
function sleep(ms: number): Promise<void> {
const gate: PromiseWithResolvers<void> = Promise.withResolvers()
setTimeout(gate.resolve, ms)
@@ -66,6 +95,7 @@ function nextCallId(): CallId {
describe('fixture server — controlled scenarios', () => {
let ctx: Context
let home: string
const fixtureConfig: Config = {
transport: 'stdio',
@@ -79,13 +109,15 @@ describe('fixture server — controlled scenarios', () => {
}
beforeAll(async () => {
ctx = await mountRegistry()
home = await mkdtemp(join(tmpdir(), 'mcp-image-e2e-'))
ctx = await mountImageRegistry(home)
await apply(ctx, fixtureConfig)
}, 30_000)
afterAll(async () => {
if (ctx) await ctx.fiber.dispose()
await sleep(200)
await rm(home, { recursive: true, force: true })
})
it('discovers all fixture tools under the server namespace', () => {
@@ -141,16 +173,23 @@ describe('fixture server — controlled scenarios', () => {
expect(result.content[0]).toMatchObject({ type: 'text' })
})
it('executes image() → image placeholder', async () => {
it('executes image() → ordered durable image content', async () => {
const result = await ctx.tools.execute({
signal: testToolSignal,
signal: testToolSignal, agent: imageAgent() as never,
callId: nextCallId(), name: 'mcp__fixture__image', arguments: {},
})
expect(result.isError).toBe(false)
const text = textOf(result.content[0])
expect(text).toContain('Here is an image:')
expect(text).toContain('[image: image/png, content discarded]')
expect(text).toContain('End of image.')
expect(result.content).toHaveLength(3)
expect(result.content[0]).toEqual({ type: 'text', text: 'Here is an image:' })
expect(result.content[2]).toEqual({ type: 'text', text: 'End of image.' })
const image = result.content[1]
if (image?.type !== 'image') throw new Error(`expected an image block, got ${JSON.stringify(image)}`)
expect(image.attachment).toMatchObject({ mediaType: 'image/png', width: 1, height: 1 })
const stored = await ctx.attachments.readImage(image.attachment)
expect(stored.data.byteLength).toBe(image.attachment.bytes)
if (result.isError) throw new Error('expected MCP image success')
expect(JSON.stringify(result.value)).toContain('iVBORw0KGgo')
expect(JSON.stringify(result.content)).not.toContain('iVBORw0KGgo')
})
})
@@ -334,13 +373,14 @@ describe('server-everything — official test server', () => {
expect(textOf(result.content[0])).toContain('10')
})
it('executes get-tiny-image → image placeholder', async () => {
it('executes get-tiny-image → explicit refusal without a durable route', async () => {
const result = await ctx.tools.execute({
signal: testToolSignal,
callId: nextCallId(), name: 'mcp__everything__get-tiny-image', arguments: {},
})
expect(result.isError).toBe(false)
expect(textOf(result.content[0])).toContain('[image: image/png, content discarded]')
expect(result.content.map(block => block.type === 'text' ? block.text : '').join('\n'))
.toContain('[image unavailable: image/png; no attachment store is mounted;')
})
})

View File

@@ -2,9 +2,14 @@ import { describe, expect, it, vi, beforeEach } from 'vitest'
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'
import { Context } from '@deepseek-ai/cordis'
import { CallId } from '@deepseek-ai/dsh-llm'
import AttachmentStore, { AttachmentId } from '@deepseek-ai/dsh-attachment'
import type { ImageAttachmentLimits, ImageAttachmentRef, SaveImageAttachment, StoredImageAttachment } from '@deepseek-ai/dsh-attachment'
import { CallId, LlmAdapter, LlmService } from '@deepseek-ai/dsh-llm'
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
import type { GenerateOptions, LlmResolvedModelInfo, StreamChunk } from '@deepseek-ai/dsh-llm'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry, { type JsonValue } from '@deepseek-ai/dsh-tools'
import type { PostToolDecision } from '@deepseek-ai/dsh-tools'
import { publicToolName, syncTools, type ToolBridgeOptions } from '@deepseek-ai/dsh-mcp-client/src/tools.ts'
import { createTransport } from '@deepseek-ai/dsh-mcp-client/src/transport.ts'
import type { Config } from '@deepseek-ai/dsh-mcp-client'
@@ -63,6 +68,79 @@ async function mountRegistry(): Promise<Context> {
return ctx
}
const IMAGE_LIMITS: ImageAttachmentLimits = {
maxImageBytes: 1024,
maxImagesPerMessage: 4,
maxMessageImageBytes: 2048,
maxImagePixels: 1024,
mediaTypes: ['image/png', 'image/jpeg', 'image/webp', 'image/gif'],
}
/** Attachment fake that records exact decoded batches while using the real batch contract. */
class RecordingAttachmentStore extends AttachmentStore {
readonly imageLimits = IMAGE_LIMITS
readonly saved: SaveImageAttachment[] = []
validateImage(_input: SaveImageAttachment): Promise<void> {
return Promise.resolve()
}
saveImage(input: SaveImageAttachment): Promise<ImageAttachmentRef> {
this.saved.push(input)
const marker = input.data[0] ?? 0
return Promise.resolve({
attachmentId: AttachmentId(`sha256:${marker.toString(16).padStart(64, '0')}`),
mediaType: input.mediaType,
bytes: input.data.byteLength,
width: 1,
height: 1,
})
}
readImage(_ref: ImageAttachmentRef): Promise<StoredImageAttachment> {
throw new Error('not used')
}
}
/** Exact-route fake used only for image-capability admission. */
class ImageCatalogAdapter extends LlmAdapter {
override resolveModel(provider: string, model: string): Promise<LlmResolvedModelInfo> {
return Promise.resolve({
provider,
id: model,
name: model,
inputModalities: model === 'vision' ? ['text', 'image'] : ['text'],
})
}
stream(_options: GenerateOptions): AsyncIterable<StreamChunk> {
throw new Error('MCP bridge tests never stream')
}
}
async function mountRichRegistry(): Promise<{ ctx: Context; attachments: RecordingAttachmentStore }> {
const ctx = await mountRegistry()
await ctx.plugin(RecordingAttachmentStore)
await ctx.plugin(LlmService)
ctx.llm.registerAdapter(['visual'], new ImageCatalogAdapter())
return { ctx, attachments: ctx.attachments as RecordingAttachmentStore }
}
/** Calling-agent stand-in with no durable request header yet. */
function agentOn(model: string | undefined = 'vision'): object {
return {
options: model === undefined ? {} : { provider: 'visual', model },
session: { requestHeader: () => undefined },
}
}
/** Require one text block and return its text for diagnostic assertions. */
function textAt(content: readonly ContentBlock[], index = 0): string {
const block = content[index]
if (block?.type !== 'text') throw new Error(`expected text content at index ${index}`)
return block.text
}
const defaultOpts: ToolBridgeOptions = {
registrationFailure: 'contain',
serverName: 'srv',
@@ -363,24 +441,306 @@ describe('tool execution', () => {
expect(result.content).toEqual([{ type: 'text', text: 'line1\nline2' }])
})
it('preserves full JSON MCP blocks while Native rendering uses placeholders', async () => {
it('preserves canonical MCP JSON while admitting an ordered mixed image result', async () => {
const rich = await mountRichRegistry()
const blocks = [
{ type: 'text', text: 'before' },
{ type: 'image', mimeType: 'image/png', data: 'base64-data', annotations: { audience: ['assistant'] } },
{ type: 'image', mimeType: 'image/png', data: 'AQ==', annotations: { audience: ['assistant'] } },
{ type: 'text', text: 'between' },
{ type: 'image', mimeType: 'image/jpeg', data: 'Ag==' },
{ type: 'text', text: 'after' },
] satisfies JsonValue[]
const client = createMockClient(
[{ name: 'img', inputSchema: { type: 'object' } }],
{ content: blocks },
)
await syncTools(client as never, ctx, defaultOpts, new Map())
const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'mcp__srv__img', arguments: {} })
await syncTools(client as never, rich.ctx, defaultOpts, new Map())
const result = await rich.ctx.tools.execute({
signal: testToolSignal,
callId: CallId('c1'),
name: 'mcp__srv__img',
arguments: {},
agent: agentOn() as never,
})
expect(result.content[0]).toEqual({ type: 'text', text: 'before\n[image: image/png, content discarded]' })
expect(result.content.map(block => block.type)).toEqual(['text', 'image', 'text', 'image', 'text'])
expect(result.content[0]).toEqual({ type: 'text', text: 'before' })
expect(result.content[2]).toEqual({ type: 'text', text: 'between' })
expect(result.content[4]).toEqual({ type: 'text', text: 'after' })
const firstImage = result.content[1]
const secondImage = result.content[3]
if (firstImage?.type !== 'image' || secondImage?.type !== 'image') throw new Error('expected ordered image blocks')
expect(firstImage.attachment.mediaType).toBe('image/png')
expect(firstImage.attachment.bytes).toBe(1)
expect(secondImage.attachment.mediaType).toBe('image/jpeg')
expect(secondImage.attachment.bytes).toBe(1)
expect(rich.attachments.saved.map(input => [...input.data])).toEqual([[1], [2]])
expect(JSON.stringify(result.content)).not.toContain('AQ==')
expect(JSON.stringify(result.content)).not.toContain('Ag==')
if (result.isError) throw new Error('expected MCP success')
expect(result.value).toEqual({ content: blocks })
})
it('keeps a valid raw image result while explicitly refusing it without a durable route', async () => {
const blocks = [{ type: 'image', mimeType: 'image/png', data: 'AQ==' }] satisfies JsonValue[]
const client = createMockClient(
[{ name: 'img', inputSchema: { type: 'object' } }],
{ content: blocks },
)
await syncTools(client as never, ctx, defaultOpts, new Map())
const result = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('no-store'),
name: 'mcp__srv__img',
arguments: {},
agent: agentOn() as never,
})
expect(result.content).toEqual([{
type: 'text',
text: '[image unavailable: image/png; no attachment store is mounted; raw image data remains available to programmatic callers]',
}])
if (result.isError) throw new Error('image refusal must preserve MCP success')
expect(result.value).toEqual({ content: blocks })
})
it('rejects a malformed image batch before storing any member', async () => {
const rich = await mountRichRegistry()
const blocks = [
{ type: 'image', mimeType: 'image/png', data: 'AQ==' },
{ type: 'image', mimeType: 'image/png', data: 'not base64' },
] satisfies JsonValue[]
const client = createMockClient(
[{ name: 'img', inputSchema: { type: 'object' } }],
{ content: blocks },
)
await syncTools(client as never, rich.ctx, defaultOpts, new Map())
const result = await rich.ctx.tools.execute({
signal: testToolSignal,
callId: CallId('bad-batch'),
name: 'mcp__srv__img',
arguments: {},
agent: agentOn() as never,
})
expect(rich.attachments.saved).toEqual([])
expect(result.content).toHaveLength(2)
expect(textAt(result.content, 0)).toContain('another image in the same result was invalid')
expect(textAt(result.content, 1)).toContain('not canonical base64')
})
it('rejects non-canonical and incomplete image blocks as one atomic batch', async () => {
const rich = await mountRichRegistry()
const client = createMockClient(
[{ name: 'img', inputSchema: { type: 'object' } }],
{ content: [
{ type: 'image', mimeType: 'image/tiff', data: 'AQ==' },
{ type: 'image', mimeType: 'image/png', data: 'AB==' },
{ type: 'image', mimeType: 'image/png' },
] },
)
await syncTools(client as never, rich.ctx, defaultOpts, new Map())
const result = await rich.ctx.tools.execute({
signal: testToolSignal,
callId: CallId('strict-batch'),
name: 'mcp__srv__img',
arguments: {},
agent: agentOn() as never,
})
expect(rich.attachments.saved).toEqual([])
expect(result.content).toHaveLength(3)
expect(textAt(result.content, 0)).toContain('not PNG, JPEG, WebP, or GIF')
expect(textAt(result.content, 1)).toContain('not canonical base64')
expect(textAt(result.content, 2)).toContain('not canonical base64')
})
it('does not admit images for a route without declared image input', async () => {
const rich = await mountRichRegistry()
const client = createMockClient(
[{ name: 'img', inputSchema: { type: 'object' } }],
{ content: [{ type: 'image', mimeType: 'image/png', data: 'AQ==' }] },
)
await syncTools(client as never, rich.ctx, defaultOpts, new Map())
const result = await rich.ctx.tools.execute({
signal: testToolSignal,
callId: CallId('text-route'),
name: 'mcp__srv__img',
arguments: {},
agent: agentOn('text') as never,
})
expect(rich.attachments.saved).toEqual([])
expect(textAt(result.content)).toContain('does not declare image input')
})
it('refuses images when the exact route is missing, unverifiable, or canceled', async () => {
const rich = await mountRichRegistry()
const client = createMockClient(
[{ name: 'img', inputSchema: { type: 'object' } }],
{ content: [{ type: 'image', mimeType: 'image/png', data: 'AQ==' }] },
)
await syncTools(client as never, rich.ctx, defaultOpts, new Map())
const noProvider = await rich.ctx.tools.execute({
signal: testToolSignal,
callId: CallId('no-provider'),
name: 'mcp__srv__img',
arguments: {},
agent: { options: { model: 'vision' }, session: { requestHeader: () => undefined } } as never,
})
expect(textAt(noProvider.content)).toContain('route could not be resolved')
const noModel = await rich.ctx.tools.execute({
signal: testToolSignal,
callId: CallId('no-model'),
name: 'mcp__srv__img',
arguments: {},
agent: { options: { provider: 'visual' }, session: { requestHeader: () => undefined } } as never,
})
expect(textAt(noModel.content)).toContain('route could not be resolved')
const noLlmCtx = await mountRegistry()
await noLlmCtx.plugin(RecordingAttachmentStore)
await syncTools(client as never, noLlmCtx, defaultOpts, new Map())
const noLlm = await noLlmCtx.tools.execute({
signal: testToolSignal,
callId: CallId('no-llm'),
name: 'mcp__srv__img',
arguments: {},
agent: agentOn() as never,
})
expect(textAt(noLlm.content)).toContain('route could not be resolved')
vi.spyOn(rich.ctx.llm, 'resolveModelInfo').mockRejectedValueOnce(new Error('catalog down'))
const unverified = await rich.ctx.tools.execute({
signal: testToolSignal,
callId: CallId('unverified'),
name: 'mcp__srv__img',
arguments: {},
agent: agentOn() as never,
})
expect(textAt(unverified.content)).toContain('route could not be verified')
vi.spyOn(rich.ctx.llm, 'resolveModelInfo').mockResolvedValueOnce({
provider: 'visual', id: 'vision', name: 'vision',
})
const unknown = await rich.ctx.tools.execute({
signal: testToolSignal,
callId: CallId('unknown-modalities'),
name: 'mcp__srv__img',
arguments: {},
agent: agentOn() as never,
})
expect(textAt(unknown.content)).toContain('does not declare image input')
const controller = new AbortController()
vi.spyOn(rich.ctx.llm, 'resolveModelInfo').mockImplementationOnce(async (provider, model) => {
controller.abort(new Error('stop'))
return { provider, id: model, name: model, inputModalities: ['text', 'image'] }
})
const canceled = await rich.ctx.tools.execute({
signal: controller.signal,
callId: CallId('canceled'),
name: 'mcp__srv__img',
arguments: {},
agent: agentOn() as never,
})
expect(canceled.isError).toBe(true)
expect(canceled.content[0]).toEqual({ type: 'text', text: 'Error: tool call aborted' })
expect(rich.attachments.saved).toEqual([])
})
it('refuses images when attachment storage rejects the admitted batch', async () => {
const rich = await mountRichRegistry()
vi.spyOn(rich.attachments, 'saveImages').mockRejectedValueOnce(new Error('disk full'))
const client = createMockClient(
[{ name: 'img', inputSchema: { type: 'object' } }],
{ content: [{ type: 'image', mimeType: 'image/png', data: 'AQ==' }] },
)
await syncTools(client as never, rich.ctx, defaultOpts, new Map())
const result = await rich.ctx.tools.execute({
signal: testToolSignal,
callId: CallId('store-rejected'),
name: 'mcp__srv__img',
arguments: {},
agent: agentOn() as never,
})
expect(textAt(result.content)).toContain('durable image storage rejected the result')
})
it('lets post-execute replacement win over a prepared image projection', async () => {
const rich = await mountRichRegistry()
rich.ctx.on('tools/post-execute', async (): Promise<PostToolDecision> => ({
kind: 'accept',
content: [{ type: 'text', text: 'policy replacement' }],
}))
const client = createMockClient(
[{ name: 'img', inputSchema: { type: 'object' } }],
{ content: [{ type: 'image', mimeType: 'image/png', data: 'AQ==' }] },
)
await syncTools(client as never, rich.ctx, defaultOpts, new Map())
const result = await rich.ctx.tools.execute({
signal: testToolSignal,
callId: CallId('replaced'),
name: 'mcp__srv__img',
arguments: {},
agent: agentOn() as never,
})
expect(rich.attachments.saved).toHaveLength(1)
expect(result.content).toEqual([{ type: 'text', text: 'policy replacement' }])
})
it('lets post-execute value replacement and blocking discard prepared projections', async () => {
const valueRich = await mountRichRegistry()
valueRich.ctx.on('tools/post-execute', async (): Promise<PostToolDecision> => ({
kind: 'accept',
value: { content: [{ type: 'text', text: 'value replacement' }] },
}))
const valueClient = createMockClient(
[{ name: 'img', inputSchema: { type: 'object' } }],
{ content: [{ type: 'image', mimeType: 'image/png', data: 'AQ==' }] },
)
await syncTools(valueClient as never, valueRich.ctx, defaultOpts, new Map())
const replaced = await valueRich.ctx.tools.execute({
signal: testToolSignal,
callId: CallId('value-replaced'),
name: 'mcp__srv__img',
arguments: {},
agent: agentOn() as never,
})
expect(replaced.content).toEqual([{ type: 'text', text: 'value replacement' }])
const blockedRich = await mountRichRegistry()
blockedRich.ctx.on('tools/post-execute', async (): Promise<PostToolDecision> => ({
kind: 'block',
feedback: [{ type: 'text', text: 'blocked by policy' }],
}))
const blockedClient = createMockClient(
[{ name: 'img', inputSchema: { type: 'object' } }],
{ content: [{ type: 'image', mimeType: 'image/png', data: 'Ag==' }] },
)
await syncTools(blockedClient as never, blockedRich.ctx, defaultOpts, new Map())
const blocked = await blockedRich.ctx.tools.execute({
signal: testToolSignal,
callId: CallId('blocked'),
name: 'mcp__srv__img',
arguments: {},
agent: agentOn() as never,
})
expect(blocked.isError).toBe(true)
expect(blocked.content).toEqual([{ type: 'text', text: 'blocked by policy' }])
})
it('preserves primitive JSON MCP blocks while Native rendering marks them unsupported', async () => {
const blocks = [42, null, ['nested']] satisfies JsonValue[]
const client = createMockClient(
@@ -396,7 +756,7 @@ describe('tool execution', () => {
expect(result.content[0]).toEqual({
type: 'text',
text: '[unsupported content type: unknown]\n[unsupported content type: unknown]\n[unsupported content type: unknown]',
text: '[unsupported MCP content block: expected an object]\n[unsupported MCP content block: expected an object]\n[unsupported MCP content block: expected an object]',
})
if (result.isError) throw new Error('expected primitive MCP blocks to remain a successful JSON value')
expect(result.value).toEqual({ content: blocks })
@@ -547,7 +907,7 @@ describe('tool execution edge cases', () => {
ctx = await mountRegistry()
})
it('handles audio content with placeholder', async () => {
it('reports unsupported audio without claiming the raw block was discarded', async () => {
const client = createMockClient(
[{ name: 'audio_tool', inputSchema: { type: 'object' } }],
{ content: [{ type: 'audio', mimeType: 'audio/mp3' }] },
@@ -556,10 +916,13 @@ describe('tool execution edge cases', () => {
await syncTools(client as never, ctx, defaultOpts, new Map())
const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'mcp__srv__audio_tool', arguments: {} })
expect(result.content[0]).toEqual({ type: 'text', text: '[audio: audio/mp3, content discarded]' })
expect(result.content[0]).toEqual({
type: 'text',
text: '[audio result unsupported: audio/mp3; raw audio data remains available to programmatic callers]',
})
})
it('handles resource content with placeholder', async () => {
it('reports unsupported embedded resources without discarding the raw block', async () => {
const client = createMockClient(
[{ name: 'res_tool', inputSchema: { type: 'object' } }],
{ content: [{ type: 'resource' }] },
@@ -568,19 +931,36 @@ describe('tool execution edge cases', () => {
await syncTools(client as never, ctx, defaultOpts, new Map())
const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'mcp__srv__res_tool', arguments: {} })
expect(result.content[0]).toEqual({ type: 'text', text: '[resource: content discarded]' })
expect(result.content[0]).toEqual({
type: 'text',
text: '[embedded resource unsupported; raw resource data remains available to programmatic callers]',
})
})
it('handles resource_link content with placeholder', async () => {
it('preserves resource-link name and URI in the model projection', async () => {
const client = createMockClient(
[{ name: 'link_tool', inputSchema: { type: 'object' } }],
{ content: [{ type: 'resource_link' }] },
{ content: [{ type: 'resource_link', name: 'Design', uri: 'https://example.test/design' }] },
)
await syncTools(client as never, ctx, defaultOpts, new Map())
const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'mcp__srv__link_tool', arguments: {} })
expect(result.content[0]).toEqual({ type: 'text', text: '[resource: content discarded]' })
expect(result.content[0]).toEqual({ type: 'text', text: 'Resource link: Design (https://example.test/design)' })
})
it('diagnoses an incomplete resource link', async () => {
const client = createMockClient(
[{ name: 'link_tool', inputSchema: { type: 'object' } }],
{ content: [{ type: 'resource_link', name: 'Missing URI' }] },
)
await syncTools(client as never, ctx, defaultOpts, new Map())
const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('missing-link'), name: 'mcp__srv__link_tool', arguments: {} })
expect(result.content[0]).toEqual({
type: 'text', text: '[resource link unavailable: the MCP block is missing its name or URI]',
})
})
it('handles unknown content types', async () => {
@@ -592,7 +972,7 @@ describe('tool execution edge cases', () => {
await syncTools(client as never, ctx, defaultOpts, new Map())
const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'mcp__srv__unknown_tool', arguments: {} })
expect(result.content[0]).toEqual({ type: 'text', text: '[unsupported content type: video]' })
expect(result.content[0]).toEqual({ type: 'text', text: '[unsupported MCP content type: video]' })
})
it('handles image with missing mimeType (buggy server)', async () => {
@@ -604,7 +984,10 @@ describe('tool execution edge cases', () => {
await syncTools(client as never, ctx, defaultOpts, new Map())
const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'mcp__srv__img2', arguments: {} })
expect(result.content[0]).toEqual({ type: 'text', text: '[image: unknown, content discarded]' })
expect(result.content[0]).toEqual({
type: 'text',
text: '[image unavailable: unknown media type; the declared media type is not PNG, JPEG, WebP, or GIF; raw image data remains available to programmatic callers]',
})
})
it('handles audio with missing mimeType (buggy server)', async () => {
@@ -616,7 +999,10 @@ describe('tool execution edge cases', () => {
await syncTools(client as never, ctx, defaultOpts, new Map())
const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'mcp__srv__audio_no_mime', arguments: {} })
expect(result.content[0]).toEqual({ type: 'text', text: '[audio: unknown, content discarded]' })
expect(result.content[0]).toEqual({
type: 'text',
text: '[audio result unsupported: unknown media type; raw audio data remains available to programmatic callers]',
})
})
it('handles text block with missing text (buggy server)', async () => {
@@ -628,7 +1014,7 @@ describe('tool execution edge cases', () => {
await syncTools(client as never, ctx, defaultOpts, new Map())
const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'mcp__srv__notext', arguments: {} })
expect(result.content[0]).toEqual({ type: 'text', text: '(notext returned no text content)' })
expect(result.content[0]).toEqual({ type: 'text', text: '(notext returned no model-visible content)' })
})
it('handles empty content array', async () => {
@@ -640,7 +1026,7 @@ describe('tool execution edge cases', () => {
await syncTools(client as never, ctx, defaultOpts, new Map())
const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'mcp__srv__empty_tool', arguments: {} })
expect(result.content[0]).toEqual({ type: 'text', text: '(empty_tool returned no text content)' })
expect(result.content[0]).toEqual({ type: 'text', text: '(empty_tool returned no model-visible content)' })
})
@@ -678,7 +1064,10 @@ describe('tool execution edge cases', () => {
const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'mcp__srv__err_notext', arguments: {} })
expect(result.isError).toBe(true)
expect(result.content[0]).toEqual({ type: 'text', text: 'Error: [image: image/png, content discarded]' })
expect(result.content[0]).toEqual({
type: 'text',
text: 'Error: [image unavailable: image/png; this result was not admitted to durable model context; raw image data remains available to programmatic callers]',
})
})