会拒绝的工具 — 模式是约定,不是保证

5 分钟
目标MCP 服务端课程第三篇。给工具加上输入模式,再写一个不信任这个模式的处理器。直到「放 0 个人进来」和「放 99 个人进来」因为两种不同的理由被拒绝为止。

第二篇的工具不收参数。这次收。而且 不信任收到的东西。

模式

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

加上它,调用方就知道该送什么。模型会照着它凑参数。

可是模式不是保证

写了 'type': 'integer' 并不会让整数真的到来。模式是 给调用方的指引,对方可能不遵守,也可能做不到。

所以处理器再看一遍。

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}))],
  );
},

两种拒绝性质不同

有两道检查,理由不一样。

n < 1 — 值本身就说不通。放 0 个人进来永远是错的。这是输入的问题。

n > waiting — 值没毛病,但眼下不可能。3 个人在等,放不出 99 个。可要是有 100 个在等,99 就正常。这是状态的问题。

之所以要分开,是因为消息不同。

count must be 1 or more     ← 再送一次还是错
only 3 waiting              ← 送 3 以内就行

一律用 isError: true 糊过去,调用方就不知道该重试还是该放弃。

为什么不能悄悄收下

也可以不拒绝,写成这样:

if (n > waiting) n = waiting;   // 悄悄给你凑上

是能跑。可是 调用方会以为放进了 99 个人。 屏上显示 99,账本上留着 3,收档对不上。而且没办法查出是在哪儿岔开的。

和仪器那篇的 12V 夹取是同一个位置。夹了,就 要说自己夹了。

校验

连着送三个请求。

ask step3 \
  '...{"name":"desk.admit","arguments":{"count":0}}}' \
  '...{"name":"desk.admit","arguments":{"count":99}}}' \
  '...{"name":"desk.admit","arguments":{"count":1}}}'

grep -q 'count must be 1 or more' captures/s3.txt || die "zero was not refused"
grep -q 'only 3 waiting'          captures/s3.txt || die "over-count was not refused"
grep -q 'waiting.*2'              captures/s3.txt || die "a valid admit did not go through"
step3  refused 0 and 99, admitted 1 -> waiting 2

第三条检查才是关键。 只有前两条的话,「什么都拒绝的工具」也能通过。会拒绝和能放行,必须一起查。

自己跑一遍

dart run bin/step3.dart
bash verify.sh

可以带走的

  1. 模式 + 处理器检查 — 模式负责指引,检查负责防守
  2. 把拒绝分成两类 — 输入的问题和状态的问题,消息应该不一样
  3. 拒绝检查 + 放行检查 — 两个都在,校验才成立

下一篇

接上画面。不是代码,而是服务端交出来的一份文档。

运行示例

makemind-academy/course_server/
git clone https://github.com/makemind-academy/course_server
cd course_server
dart pub get
dart run bin/step3.dart
在 GitHub 上打开 →