Skip to content

Commit 948df8f

Browse files
committed
fix(agentic): do not advertise an output schema devframe cannot derive
An RPC function whose `returns` validator has no Standard JSON Schema converter degraded to a permissive `{ type: 'object' }` schema. That is fine as documentation, but it is advertised as the MCP `outputSchema`, which obliges the tool to return a matching object on every call. Any such function returning an array or a primitive therefore failed `tools/call` with -32602, the SDK rejecting the server's own response: Invalid tools/call result: expected record, received array Return no output schema instead when the converter is absent, so the result travels as text content only. Validators with a native converter (zod 4) are unaffected.
1 parent 18fa60e commit 948df8f

3 files changed

Lines changed: 42 additions & 2 deletions

File tree

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

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { DevframeHost } from 'devframe/types'
22
import { Client, InMemoryTransport } from '@modelcontextprotocol/client'
33
import { createHostContext } from 'devframe/node'
4+
import * as v from 'valibot'
45
import { describe, expect, it } from 'vitest'
56
import { buildMcpServerFromContext } from '../build-server'
67

@@ -165,6 +166,35 @@ describe('mcp adapter (in-memory)', () => {
165166
}
166167
})
167168

169+
it('calls an rpc-backed tool whose return schema has no native converter', async () => {
170+
const { ctx, client, cleanup } = await bootPair()
171+
try {
172+
ctx.rpc.register({
173+
name: 'list-things',
174+
type: 'query',
175+
jsonSerializable: true,
176+
args: [],
177+
returns: v.array(v.object({ id: v.string() })),
178+
agent: { description: 'Lists things.' },
179+
handler: () => [{ id: 'a' }, { id: 'b' }],
180+
} as never)
181+
182+
const listed = await client.listTools()
183+
const tool = listed.tools.find(t => t.name.endsWith('list-things'))
184+
expect(tool).toBeDefined()
185+
expect(tool!.outputSchema).toBeUndefined()
186+
187+
const result = await client.callTool({ name: tool!.name, arguments: {} })
188+
expect(result.isError).toBeFalsy()
189+
const content = result.content as Array<{ type: string, text: string }>
190+
expect(JSON.parse(content[0]!.text)).toEqual([{ id: 'a' }, { id: 'b' }])
191+
expect(result.structuredContent).toBeUndefined()
192+
}
193+
finally {
194+
await cleanup()
195+
}
196+
})
197+
168198
it('coerces non-JSON values returned from a tool', async () => {
169199
const { ctx, client, cleanup } = await bootPair()
170200
try {

‎packages/devframe/src/agent/__tests__/to-json-schema.test.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ describe('returnToJsonSchema', () => {
4949
.toEqual({ type: 'object', properties: { ok: { type: 'boolean' } } })
5050
})
5151

52-
it('falls back to permissive for validators without a native converter', () => {
53-
expect(returnToJsonSchema(v.object({ ok: v.boolean() }))).toEqual(PERMISSIVE)
52+
it('yields no schema for validators without a native converter', () => {
53+
expect(returnToJsonSchema(v.object({ ok: v.boolean() }))).toBeUndefined()
54+
expect(returnToJsonSchema(v.array(v.object({ ok: v.boolean() })))).toBeUndefined()
5455
})
5556
})

‎packages/devframe/src/agent/to-json-schema.ts‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,20 @@ function safeToJsonSchema(schema: StandardSchemaV1): unknown {
2929

3030
/**
3131
* JSON Schema for an RPC return value on the agent/MCP surface.
32+
*
33+
* Unlike args, a return value has no permissive fallback: the schema is
34+
* advertised as an MCP `outputSchema`, which obliges the tool to return a
35+
* matching object on every call. A validator without a native converter
36+
* (e.g. valibot) yields no output schema rather than an unfounded object
37+
* one, so array- and primitive-returning tools still work.
3238
* @internal
3339
*/
3440
export function returnToJsonSchema(schema: StandardSchemaV1 | undefined): unknown {
3541
if (!schema)
3642
return undefined
43+
const standard = schema['~standard'] as MaybeJsonSchema
44+
if (!standard.jsonSchema)
45+
return undefined
3746
return safeToJsonSchema(schema)
3847
}
3948

0 commit comments

Comments
 (0)