거절하는 도구 — 스키마는 약속이지 보장이 아니다
5 분목표MCP 서버 강좌 3편. 입력 스키마를 붙이고, 그 스키마를 믿지 않는 핸들러를 쓴다. 0명을 들여보내라는 요청과 99명을 들여보내라는 요청이 각각 다른 이유로 거절되는 것까지.
2편의 도구는 인자를 안 받았다. 이번엔 받는다. 그리고 받은 걸 안 믿는다.
스키마
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가져갈 것
- 스키마 + 핸들러 검사 — 스키마는 안내, 검사는 방어
- 거절을 두 종류로 — 입력의 문제와 상태의 문제는 메시지가 달라야 한다
- 거절 검사 + 통과 검사 — 둘 다 있어야 검증이 성립한다
다음 편
화면을 붙인다. 코드가 아니라 서버가 내주는 문서로.
샘플 실행하기
makemind-academy/course_server/git clone https://github.com/makemind-academy/course_server cd course_server dart pub get dart run bin/step3.dart