|
2 | 2 | outline: deep |
3 | 3 | --- |
4 | 4 |
|
5 | | -# DF0019: Non-JSON Value in JSON-Serializable RPC |
| 5 | +# DF0019: Agent Requires JSON-Serializable RPC |
6 | 6 |
|
7 | 7 | > Package: `devframe` |
8 | 8 |
|
9 | 9 | ## Message |
10 | 10 |
|
11 | | -> RPC function "`{name}`" declares `jsonSerializable: true` but the value at "`{path}`" is a `{type}`. |
| 11 | +> RPC function "`{name}`" has `agent` set but `jsonSerializable` is not `true` — MCP requires JSON-serializable data. |
12 | 12 |
|
13 | 13 | ## Cause |
14 | 14 |
|
15 | | -The function is declared `jsonSerializable: true`, which means its args and return value are encoded with strict `JSON.stringify` (both on the wire and in build dumps). The strict serializer rejects any value that JSON cannot round-trip losslessly: |
| 15 | +The `agent` field exposes an RPC function as an MCP tool. MCP and the underlying schema-conversion path (`@valibot/to-json-schema`) only consume JSON-shaped data. Functions whose payloads can include `Map`, `Set`, `Date`, `BigInt`, circular references, or class instances cannot be safely advertised to agents. |
16 | 16 |
|
17 | | -- `Map`, `Set`, `WeakMap`, `WeakSet` |
18 | | -- `Date` (silently coerced to ISO string by JSON) |
19 | | -- `BigInt` |
20 | | -- circular references |
21 | | -- non-plain class instances |
22 | | -- `undefined` leaves |
23 | | -- `Symbol` |
24 | | -- `Function` |
25 | | - |
26 | | -When the strict serializer encounters one of these, it throws synchronously at the offending call rather than producing a corrupt payload. |
| 17 | +A registered function is rejected when `agent` is present and `jsonSerializable` is not explicitly `true`. |
27 | 18 |
|
28 | 19 | ## Example |
29 | 20 |
|
30 | 21 | ```ts |
31 | 22 | defineRpcFunction({ |
32 | | - name: 'my-plugin:graph', |
33 | | - jsonSerializable: true, |
34 | | - handler: () => ({ |
35 | | - nodes: new Map([['a', 1]]), // ← throws DF0019 with type=Map, path="nodes" |
36 | | - }), |
| 23 | + name: 'my-plugin:summary', |
| 24 | + agent: { description: 'Returns a summary' }, |
| 25 | + // missing `jsonSerializable: true` → registration throws DF0019 |
| 26 | + handler: () => ({ items: [1, 2, 3] }), |
37 | 27 | }) |
38 | 28 | ``` |
39 | 29 |
|
40 | 30 | ## Fix |
41 | 31 |
|
42 | | -Either drop `jsonSerializable: true` so the function uses `structured-clone-es` (round-trips `Map`, `Set`, etc.): |
| 32 | +Either declare the payload as JSON-safe: |
43 | 33 |
|
44 | 34 | ```ts |
45 | 35 | defineRpcFunction({ |
46 | | - name: 'my-plugin:graph', |
47 | | - // jsonSerializable: false (default) — Map/Set survive the wire and the dump |
48 | | - handler: () => ({ |
49 | | - nodes: new Map([['a', 1]]), |
50 | | - }), |
| 36 | + name: 'my-plugin:summary', |
| 37 | + jsonSerializable: true, |
| 38 | + agent: { description: 'Returns a summary' }, |
| 39 | + handler: () => ({ items: [1, 2, 3] }), |
51 | 40 | }) |
52 | 41 | ``` |
53 | 42 |
|
54 | | -Or convert the payload to a JSON-safe shape (e.g. an array of entries, an ISO string, a plain object) before returning. Note: removing `jsonSerializable: true` also disables `agent` exposure; if you need MCP, you must use a JSON-safe shape. |
| 43 | +Or remove `agent` to keep the function as an internal RPC (no agent exposure): |
| 44 | + |
| 45 | +```ts |
| 46 | +defineRpcFunction({ |
| 47 | + name: 'my-plugin:summary', |
| 48 | + handler: () => new Map([['a', 1]]), |
| 49 | +}) |
| 50 | +``` |
55 | 51 |
|
56 | 52 | ## Source |
57 | 53 |
|
58 | | -`packages/devframe/src/rpc/serialization.ts` |
| 54 | +`packages/devframe/src/rpc/collector.ts` |
0 commit comments