Skip to content

Commit 7aa26b7

Browse files
committed
fix: infer JSON serialization for agent RPCs
1 parent 5f6d5be commit 7aa26b7

6 files changed

Lines changed: 30 additions & 25 deletions

File tree

‎docs/content/1.guide/3.rpc.md‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,6 @@ Add an `agent` field to expose the function to coding agents over MCP:
193193
defineRpcFunction({
194194
name: 'get-modules',
195195
type: 'query',
196-
jsonSerializable: true,
197196
args: [v.object({ limit: v.number() })],
198197
returns: v.array(v.object({ id: v.string(), size: v.number() })),
199198
agent: {
@@ -207,7 +206,7 @@ defineRpcFunction({
207206
})
208207
```
209208

210-
Exposing a function over MCP requires `jsonSerializable: true`.
209+
The `agent` field implicitly enables strict JSON serialization because MCP consumes JSON-shaped data. Set `jsonSerializable: true` directly when an RPC-only function also benefits from that contract.
211210

212211
## What's next
213212

‎docs/content/6.errors/DF0019.md‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,30 @@
11
---
22
title: 'DF0019: Agent Requires JSON-Serializable RPC'
3-
description: 'RPC function "{name}" has agent set but jsonSerializable is not true; MCP requires JSON-serializable data.'
3+
description: 'RPC function "{name}" has agent set but jsonSerializable is false; MCP requires JSON-serializable data.'
44
---
55

66
## Message
77

8-
> RPC function "`{name}`" has `agent` set but `jsonSerializable` is not `true`; MCP requires JSON-serializable data.
8+
> RPC function "`{name}`" has `agent` set but `jsonSerializable` is `false`; MCP requires JSON-serializable data.
99
1010
## Cause
1111

12-
The `agent` field exposes an RPC function as an MCP tool, and MCP only consumes JSON-shaped data. A function with `agent` set is rejected unless it also declares `jsonSerializable: true`.
12+
The `agent` field exposes an RPC function as an MCP tool and implicitly enables strict JSON serialization. An explicit `jsonSerializable: false` conflicts with MCP's JSON-shaped data.
1313

1414
## Example
1515

1616
```ts
1717
defineRpcFunction({
1818
name: 'my-plugin:summary',
1919
agent: { description: 'Returns a summary' },
20-
handler: () => ({ items: [1, 2, 3] }), // ✗ throws DF0019: missing jsonSerializable: true
20+
jsonSerializable: false, // ✗ throws DF0019
21+
handler: () => ({ items: [1, 2, 3] }),
2122
})
2223
```
2324

2425
## Fix
2526

26-
Set `jsonSerializable: true` if the payload is JSON-safe, or remove `agent` to keep it RPC-only.
27+
Remove `jsonSerializable: false` to use the implicit JSON contract, or remove `agent` to keep the function RPC-only.
2728

2829
```ts
2930
defineRpcFunction({
@@ -36,4 +37,4 @@ defineRpcFunction({
3637

3738
## Source
3839

39-
- [`packages/devframe/src/rpc/collector.ts`](https://github.com/devframes/devframe/blob/main/packages/devframe/src/rpc/collector.ts): `RpcFunctionsCollectorBase.register()` throws `DF0019` when a definition has `agent` set but is not declared `jsonSerializable: true`.
40+
- [`packages/devframe/src/rpc/collector.ts`](https://github.com/devframes/devframe/blob/main/packages/devframe/src/rpc/collector.ts): `RpcFunctionsCollectorBase.register()` throws `DF0019` when a definition combines `agent` with `jsonSerializable: false`.

‎packages/devframe/src/rpc/collector.test.ts‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@ import { describe, expect, it, vi } from 'vitest'
22
import { RpcFunctionsCollectorBase } from './collector'
33

44
describe('agent gating (DF0019)', () => {
5-
it('rejects registration when agent is set without jsonSerializable: true', () => {
5+
it('infers jsonSerializable: true when agent is set', () => {
66
const collector = new RpcFunctionsCollectorBase({})
7-
expect(() => collector.register({
7+
collector.register({
88
name: 'plugin:fn',
99
agent: { description: 'x' },
1010
handler: () => 0,
11-
} as any)).toThrowError(/MCP requires JSON-serializable/)
11+
} as any)
12+
expect(collector.get('plugin:fn')?.jsonSerializable).toBe(true)
1213
})
1314

1415
it('rejects when agent + jsonSerializable: false', () => {
@@ -40,14 +41,15 @@ describe('agent gating (DF0019)', () => {
4041
} as any)).not.toThrow()
4142
})
4243

43-
it('also enforces the gate on update()', () => {
44+
it('also infers jsonSerializable: true on update()', () => {
4445
const collector = new RpcFunctionsCollectorBase({})
4546
collector.register({ name: 'plugin:fn', handler: () => 0 } as any)
46-
expect(() => collector.update({
47+
collector.update({
4748
name: 'plugin:fn',
4849
agent: { description: 'x' },
4950
handler: () => 0,
50-
} as any)).toThrowError(/MCP requires JSON-serializable/)
51+
} as any)
52+
expect(collector.get('plugin:fn')?.jsonSerializable).toBe(true)
5153
})
5254
})
5355

‎packages/devframe/src/rpc/collector.ts‎

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export class RpcFunctionsCollectorBase<
4343
if (this.definitions.has(fn.name) && !force) {
4444
throw diagnostics.DF0021({ name: fn.name })
4545
}
46-
assertAgentJsonSerializable(fn)
46+
fn = normalizeAgentJsonSerializable(fn)
4747
this.definitions.set(fn.name, fn)
4848
this._onChanged.forEach(cb => cb(fn.name))
4949
}
@@ -52,7 +52,7 @@ export class RpcFunctionsCollectorBase<
5252
if (!this.definitions.has(fn.name) && !force) {
5353
throw diagnostics.DF0022({ name: fn.name })
5454
}
55-
assertAgentJsonSerializable(fn)
55+
fn = normalizeAgentJsonSerializable(fn)
5656
this.definitions.set(fn.name, fn)
5757
this._onChanged.forEach(cb => cb(fn.name))
5858
}
@@ -94,9 +94,12 @@ export class RpcFunctionsCollectorBase<
9494
}
9595
}
9696

97-
function assertAgentJsonSerializable(
98-
fn: RpcFunctionDefinition<string, any, any, any, any, any, any>,
99-
): void {
100-
if (fn.agent && fn.jsonSerializable !== true)
97+
function normalizeAgentJsonSerializable<T extends RpcFunctionDefinition<string, any, any, any, any, any, any>>(
98+
fn: T,
99+
): T {
100+
if (fn.agent && fn.jsonSerializable === false)
101101
throw diagnostics.DF0019({ name: fn.name })
102+
if (fn.agent && fn.jsonSerializable === undefined)
103+
return { ...fn, jsonSerializable: true }
104+
return fn
102105
}

‎packages/devframe/src/rpc/diagnostics.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ export const diagnostics = defineDiagnostics({
55
codes: {
66
DF0019: {
77
why: (p: { name: string }) =>
8-
`RPC function "${p.name}" has \`agent\` set but \`jsonSerializable\` is not \`true\`; MCP requires JSON-serializable data.`,
9-
fix: 'Set `jsonSerializable: true` if the payload is JSON-safe, or remove `agent` to keep it RPC-only.',
8+
`RPC function "${p.name}" has \`agent\` set but \`jsonSerializable\` is \`false\`; MCP requires JSON-serializable data.`,
9+
fix: 'Remove `jsonSerializable: false`, or remove `agent` to keep it RPC-only.',
1010
},
1111
DF0020: {
1212
why: (p: { name: string, type: string, path: string }) =>

‎packages/devframe/src/rpc/types.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ export interface RpcFunctionDefinitionBase {
178178
* - `true`: args and return are encoded with strict `JSON.stringify`
179179
* on the wire and on disk. Misshapen values throw `DF0019` at the
180180
* sender, surfacing the bug *during the offending call* rather than
181-
* silently coercing to `{}` later. Required for `agent` exposure.
181+
* silently coercing to `{}` later. Inferred for `agent` exposure.
182182
* - `false` (default): payloads use `structured-clone-es`, which
183183
* round-trips Maps/Sets/cycles. Functions in this mode cannot be
184184
* exposed via the `agent` field; registration throws `DF0018`.
@@ -249,7 +249,7 @@ export type RpcFunctionDefinition<
249249
* (no Map/Set/Date/BigInt/cycles/class instances/undefined/Symbol/Function).
250250
*
251251
* - `true`: wire and dump use strict `JSON.stringify`; misshapen
252-
* values throw `DF0019` at the call site. Required for `agent`.
252+
* values throw `DF0019` at the call site. Inferred for `agent`.
253253
* - `false` (default): `structured-clone-es` round-trips fancy
254254
* types. Cannot be `agent`-exposed (registration throws `DF0018`).
255255
*/
@@ -295,7 +295,7 @@ export type RpcFunctionDefinition<
295295
* (no Map/Set/Date/BigInt/cycles/class instances/undefined/Symbol/Function).
296296
*
297297
* - `true`: wire and dump use strict `JSON.stringify`; misshapen
298-
* values throw `DF0019` at the call site. Required for `agent`.
298+
* values throw `DF0019` at the call site. Inferred for `agent`.
299299
* - `false` (default): `structured-clone-es` round-trips fancy
300300
* types. Cannot be `agent`-exposed (registration throws `DF0018`).
301301
*/

0 commit comments

Comments
 (0)