Skip to content

Agent tools from RPC args reach MCP clients as arg0/arg1 with no names or descriptions #420

Description

@erkamyaman

What happens

When an RPC function with args is exposed through agent, the MCP (and WebMCP) input schema names each positional arg arg0, arg1, and so on (argsToJsonSchema in packages/devframe/src/agent/to-json-schema.ts). There is no way to give an arg a name or a description.

With valibot it gets worse. valibot has no native Standard JSON Schema converter, so each arg falls back to { type: 'object', additionalProperties: true }. The agent sees one opaque arg0 object and nothing about what goes inside it.

Repro

This is the example from the agent-native guide:

import * as v from 'valibot'
import { defineRpcFunction } from 'devframe'

export const getSessionSummary = defineRpcFunction({
  name: 'rolldown-get-session-summary',
  type: 'query',
  args: [v.object({ sessionId: v.string() })],
  agent: {
    description: 'Summarize a Rolldown build session.',
  },
  setup: () => ({
    handler: async ({ sessionId }) => ({ sessionId }),
  }),
})

tools/list returns this input schema:

{
  "type": "object",
  "properties": {
    "arg0": { "type": "object", "additionalProperties": true }
  },
  "required": ["arg0"],
  "additionalProperties": false
}

The agent has to guess that it should send { "arg0": { "sessionId": "..." } }. The existing test falls back to a permissive object for validators without a native converter (valibot) in packages/devframe/src/agent/__tests__/to-json-schema.test.ts pins this output.

With zod 4 the types come through, but the property is still arg0, and a description only shows up if it is set on the schema itself.

Expected

The agent sees a named parameter with a type and a description, for example:

{
  "type": "object",
  "properties": {
    "sessionId": { "type": "string", "description": "Session id from the session list." }
  },
  "required": ["sessionId"]
}

Why it matters

Agents pick and fill tools from the schema alone. arg0 with an open object tells them nothing, so they guess, call the tool wrong, or skip it. We're building Angular DevTools on devframe, and we stopped declaring agent tools as RPC args for this reason. We register each one with ctx.agent.registerTool and a hand-written inputSchema, so the schema and the handler types can drift.

Possible directions

Open to whichever fits devframe best:

  1. Declare names on the agent options. For example agent.params: [{ name: 'sessionId', description: '...' }], one entry per positional arg. argsToJsonSchema uses those names, and toolInputToRpcArgs maps them back to positions. Function parameter names are not reliable at runtime (the handler is built in setup and may be minified), so they need to be declared.
  2. Flatten a single object arg. When an RPC has exactly one arg and its JSON Schema is an object, advertise its properties at the top level and wrap the payload back into arg0 on invoke. This covers the common case with no new options, but it changes the wire shape, so it may need an opt in.
  3. Allow agent.inputSchema on RPC functions, like registerTool already allows. This is the smallest change and also gives valibot users a way out, though it keeps the drift problem.

I can send a PR once a direction is agreed.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions