Build

A Tool That Refuses — a Schema Is a Promise, Not a Guarantee

By makemind · Feb 20, 2026

The tool in part 2 took no arguments. Now it does. And it does not trust what it gets.

The schema

inputSchema: const {
  'type': 'object',
  'properties': {
    'count': {'type': 'integer', 'description': 'How many to admit, 1 or more'},
  },
  'required': ['count'],
},

With this the caller knows what to send. A model shapes its arguments to it.

But a schema is not a guarantee

Writing 'type': 'integer' does not make an integer arrive. A schema is guidance to the caller, and the caller may not follow it, or may not be able to.

So the handler looks again.

handler: (args) async {
  final n = args['count'];
  // The schema says integer; the caller may still send anything.
  if (n is! int || n < 1) {
    return CallToolResult(
      content: [TextContent(text: 'desk.admit: count must be 1 or more')],
      isError: true,
    );
  }
  // And the desk cannot admit people who are not there.
  if (n > waiting) {
    return CallToolResult(
      content: [TextContent(text: 'only $waiting waiting')],
      isError: true,
    );
  }
  waiting -= n;
  return CallToolResult(
    content: [TextContent(text: jsonEncode({'waiting': waiting}))],
  );
},

The two refusals differ in kind

There are two checks, for different reasons.

n < 1 — the value itself makes no sense. Admitting zero people is always wrong. A problem with the input.

n > waiting — the value is fine but impossible right now. You cannot admit 99 from a queue of 3. With 100 waiting, 99 is normal. A problem with the state.

The distinction matters because of the message.

count must be 1 or more     ← sending it again is wrong the same way
only 3 waiting              ← send 3 or fewer and it works

Collapse both into isError: true and the caller cannot tell whether to retry or give up.

This content requires Developer or above

Sign in and upgrade your plan to continue reading.

View Plans
Twitter