빌드

부르고 받는다 — 거절은 예외가 아니다

작성: makemind · 2026년 4월 3일

2편에서 무엇이 있는지 알았다. 부른다.

결과는 값이 아니다

final ok = await client.callTool('desk.admit', {'count': 1});

돌아오는 건 숫자가 아니라 콘텐츠 목록이다. 텍스트일 수도, 이미지일 수도, 여러 개일 수도 있다. 그래서 꺼내는 자리를 한 곳에 둔다.

Map<String, dynamic> decode(CallToolResult r) {
  final first = r.content.first;
  if (first is! TextContent) {
    throw StateError('expected text content, got ${first.runtimeType}');
  }
  return jsonDecode(first.text) as Map<String, dynamic>;
}

is! TextContent 검사를 빼고 바로 캐스트하면, 서버가 언젠가 이미지를 섞어 보낼 때 엉뚱한 자리에서 터진다. 여기서 걸러야 스택이 읽힌다.

admit 1 -> {waiting: 2}

거절은 throw 로 오지 않는다

서버 3편에서 만든 거절을 불러 본다.

final refused = await client.callTool('desk.admit', {'count': 99});
final text = (refused.content.first as TextContent).text;
stdout.writeln('admit 99 -> isError=${refused.isError} "$text"');
admit 99 -> isError=true "only 2 waiting"

예외가 안 났다. await 가 정상 완료되고, 결과에 isError: true 가 붙어 있고, 이유가 텍스트로 들어 있다.

이걸 모르면 이렇게 쓴다.

try {
  await client.callTool('desk.admit', {'count': 99});
  // 여기까지 왔으니 성공
} catch (e) {
  // 거절은 여기로 온다 ... 고 생각하지만 안 온다
}

거절이 catch 로 안 가므로 성공으로 처리된다. 화면에 "99명 입장" 이 뜨고, 서버는 아무도 안 들여보냈다.

이 콘텐츠는 개발자 이상이 필요합니다

로그인 후 플랜을 업그레이드하면 계속 읽을 수 있습니다.

플랜 보기
Twitter