Published on

Claude Tool Use 400 invalid_tool_schema 해결 가이드

Authors

서버/에이전트에 Claude Tool Use를 붙이다 보면 가장 흔히 맞닥뜨리는 에러 중 하나가 400 invalid_tool_schema입니다. 메시지는 짧지만 원인은 다양합니다. 대부분 도구(tool) 정의의 JSON Schema가 Claude가 기대하는 형태와 미묘하게 어긋나서 발생하며, 특히 타입/required/추가 속성 허용 여부 같은 “스키마 엄격성”에서 많이 터집니다.

이 글에서는 (1) 어떤 경우에 invalid_tool_schema가 나는지, (2) Claude Tool 스키마를 올바르게 작성하는 패턴, (3) 실제로 빠르게 원인을 좁히는 디버깅 절차를 코드와 함께 정리합니다.

> 참고: 네트워크/프록시 환경에서 요청이 변형되거나 헤더가 꼬여 진단이 어려운 경우도 있습니다. OAuth나 리다이렉트, 프록시 계층을 함께 운영 중이라면 Proxy 뒤 Nginx에서 OAuth 리다이렉트 URI 불일치 해결처럼 “중간 계층이 요청을 어떻게 바꾸는지” 점검하는 습관이 도움이 됩니다.

invalid_tool_schema란 무엇인가

invalid_tool_schema는 말 그대로 tool 정의의 schema가 유효하지 않다는 뜻입니다. 여기서 schema는 일반적으로 다음을 포함합니다.

  • name: 도구 이름(식별자)
  • description: 모델이 도구를 언제 써야 하는지 이해하기 위한 설명
  • input_schema: 도구 입력을 기술하는 JSON Schema

문제는 JSON Schema 자체가 “문법적으로 JSON이 맞다”와 “Claude가 지원하는 JSON Schema subset 규칙을 만족한다”가 다를 수 있다는 점입니다. 즉, 다른 JSON Schema validator에서는 통과하지만 Claude에서는 거절될 수 있습니다.

가장 흔한 원인 TOP 8

아래 항목이 실제로 가장 많이 원인입니다. 하나씩 체크하면 대부분 해결됩니다.

1) input_schema 최상위 type이 object가 아님

Tool 입력은 대부분 type: "object"를 기대합니다.

  • 잘못된 예: type: "string" 또는 배열을 최상위로 둠
  • 해결: 최상위는 object로 두고 그 안에 properties로 구성

2) properties는 있는데 required가 없거나 불일치

  • required에 선언한 필드가 properties에 없음
  • 혹은 반대로 필수로 받아야 하는데 required를 빼서 모델이 빈 입력을 만들게 됨

특히 required에 오타가 있으면 즉시 invalid_tool_schema로 떨어지는 경우가 많습니다.

3) JSON Schema 키 오타/잘못된 위치

대표적으로:

  • inputSchema(camelCase)로 작성
  • parameters 같은 OpenAI 스타일 키를 섞어 씀
  • requiredproperties 내부에 넣음

Claude는 “기대하는 구조”가 꽤 엄격하므로, 키 이름/위치가 다르면 바로 실패합니다.

4) type/enum/items 조합이 모순

예:

  • type: "string"인데 items를 둠(배열 전용)
  • type: "array"인데 items가 없음
  • enum이 문자열 목록인데 type을 number로 둠

5) additionalProperties 처리 문제

엄격 모드로 운영하려고 additionalProperties: false를 켜는 경우가 많은데, 이때 properties에 정의되지 않은 키가 들어올 여지가 있으면 모델이 도구 호출을 구성하다가 실패하거나, 스키마 자체가 특정 조건에서 거절되는 경우가 있습니다.

권장 패턴은:

  • 초기에 디버깅 단계: additionalProperties: true 또는 생략
  • 안정화 후: additionalProperties: false + 모든 입력 키를 명시

6) nullable 표현 방식 혼용

JSON Schema에서 nullable은 OpenAPI 스타일이고, 표준 JSON Schema에서는 보통 type: ["string", "null"]처럼 표현합니다. 일부 조합은 Claude에서 거절될 수 있습니다.

권장:

  • null 허용이 꼭 필요하면 type: ["string", "null"] 패턴을 사용
  • 아니면 아예 null을 금지하고 빈 문자열/옵션 필드로 처리

7) description/metadata에 너무 복잡한 구조를 넣음

description에는 텍스트만 두는 것이 안전합니다. 객체/배열을 넣거나, 스키마 외부에 커스텀 키를 마구 추가하면 거절될 수 있습니다.

8) 도구 이름 규칙 위반(문자/길이)

도구 이름에 공백/특수문자/너무 긴 문자열 등을 넣으면 플랫폼에 따라 거부될 수 있습니다.

권장:

  • snake_case 또는 kebab-case 대신 보통 snake_case/영문+숫자+언더스코어 중심
  • 예: search_docs, get_user_profile

올바른 Tool 스키마 예시(정석 템플릿)

아래는 “최소한으로 안전한” 입력 스키마 템플릿입니다. 문제 재현/디버깅 시 이 형태로 먼저 단순화한 뒤, 점진적으로 확장하는 방식이 가장 빠릅니다.

{
  "name": "search_docs",
  "description": "문서 저장소에서 키워드로 검색합니다.",
  "input_schema": {
    "type": "object",
    "properties": {
      "query": {
        "type": "string",
        "description": "검색 키워드"
      },
      "top_k": {
        "type": "integer",
        "description": "반환할 최대 결과 수",
        "minimum": 1,
        "maximum": 20
      }
    },
    "required": ["query"],
    "additionalProperties": false
  }
}

핵심 포인트는 다음입니다.

  • 최상위 type: object
  • properties에 모든 필드 정의
  • required는 실제 존재하는 키만
  • additionalProperties: false를 켰다면 정의되지 않은 키는 절대 못 들어오게 설계

Node.js(Typescript)에서 invalid_tool_schema 빠르게 재현/검증하기

실제 장애 상황에서는 “내가 보낸 payload가 무엇인지”부터 확정해야 합니다. 프록시/미들웨어/로깅 필터가 JSON을 바꾸는 경우도 흔합니다.

아래는 요청 직전 payload를 그대로 덤프하고, 스키마를 최소 validator로 한 번 더 확인하는 예시입니다.

import Ajv from "ajv";

const ajv = new Ajv({ allErrors: true });

// Claude tool input_schema 자체를 검증하는 게 아니라,
// 우리가 의도한 형태(최상위 object, required 일치 등)를 사전 점검하는 용도
function precheckToolSchema(tool: any) {
  if (!tool?.name || typeof tool.name !== "string") throw new Error("tool.name missing");
  if (!tool?.input_schema) throw new Error("tool.input_schema missing");
  if (tool.input_schema.type !== "object") throw new Error("input_schema.type must be object");
  const props = tool.input_schema.properties ?? {};
  const req: string[] = tool.input_schema.required ?? [];
  for (const k of req) {
    if (!props[k]) throw new Error(`required key not in properties: ${k}`);
  }

  // 추가로: input_schema가 JSON Schema로서 크게 깨지지 않았는지 확인(부분적)
  const validate = ajv.compile(tool.input_schema);
  // 샘플 입력으로 한 번 돌려보기(필수값만)
  const sample: any = {};
  for (const k of req) sample[k] = "__sample__";
  validate(sample);
  if (validate.errors) {
    throw new Error("AJV errors: " + JSON.stringify(validate.errors));
  }
}

const tool = {
  name: "search_docs",
  description: "문서 저장소에서 키워드로 검색합니다.",
  input_schema: {
    type: "object",
    properties: {
      query: { type: "string" },
      top_k: { type: "integer", minimum: 1, maximum: 20 }
    },
    required: ["query"],
    additionalProperties: false
  }
};

precheckToolSchema(tool);
console.log("tool schema ok");

이 사전 점검만으로도 “required 오타”, “최상위 타입 오류”, “properties 누락” 같은 1차 사고는 대부분 잡힙니다.

Python에서 흔한 실수: dict를 JSON으로 직렬화하는 과정

Python에서는 None이 JSON으로 직렬화되면 null이 됩니다. 의도치 않게 null이 들어가면서 스키마와 충돌하는 경우가 있습니다. 또한 로깅/저장 과정에서 인코딩 문제가 섞이면 payload 자체가 깨져 보이기도 합니다.

인코딩 이슈가 의심되면 관련해서는 Python UnicodeDecodeError 해결 - 인코딩 탐지·일괄변환처럼 “입출력 경로”를 먼저 정리해두면 디버깅 비용이 줄어듭니다.

아래는 tool 정의를 JSON으로 안전하게 출력(검증)하는 예시입니다.

import json

tool = {
  "name": "search_docs",
  "description": "문서 저장소에서 키워드로 검색합니다.",
  "input_schema": {
    "type": "object",
    "properties": {
      "query": {"type": "string"},
      "top_k": {"type": "integer", "minimum": 1, "maximum": 20}
    },
    "required": ["query"],
    "additionalProperties": False
  }
}

# ensure_ascii=False로 실제 한글이 깨지지 않게 확인
payload = json.dumps(tool, ensure_ascii=False)
print(payload)

# 역직렬화로 JSON round-trip이 되는지 확인
json.loads(payload)

여기서 additionalProperties는 JSON에서는 boolean이므로 False가 맞습니다. 다만 일부 코드에서 문자열로 들어가면("false") 스키마가 깨질 수 있으니 주의하세요.

실전 디버깅 체크리스트(장애 대응 순서)

invalid_tool_schema는 “스키마가 틀렸다”가 핵심이므로, 아래 순서로 좁히는 게 가장 빠릅니다.

1) 요청 직전의 tools payload를 그대로 로그로 남기기

  • 애플리케이션 내부 객체가 아니라 실제 전송되는 JSON 문자열을 덤프
  • 프록시/게이트웨이/미들웨어가 있다면 그 구간에서 body 변형 여부 확인

2) tool을 1개만 남기고 최소 스키마로 축소

  • 문제가 나는 tool이 여러 개면 하나씩 분리
  • input_schema를 위의 “정석 템플릿” 수준으로 단순화

3) required/properties 불일치부터 확인

  • required 오타가 가장 치명적이고 흔함
  • 필수 필드를 줄이고 하나씩 늘리며 확인

4) additionalProperties를 잠시 완화

  • 우선 additionalProperties를 제거하거나 true로 두고 통과 여부 확인
  • 통과하면 이후 false로 되돌리며 누락된 properties를 채움

5) enum/type/items 조합 검증

  • 배열이면 items 필수
  • enum 값의 타입과 type 일치

6) null 허용 여부 재설계

  • null이 들어올 가능성이 있으면 스키마에 반영
  • 아니면 애초에 null을 만들지 않도록 입력 전처리

장애를 줄이는 운영 팁

  • 도구 스키마를 코드에서 생성하지 말고(동적 생성 최소화), 정적 JSON/타입 정의로 관리한 뒤 PR 리뷰 대상으로 삼는 것이 안전합니다.
  • 스키마 변경 시 CI에서 “required/properties 일치 검사” 같은 린트 단계를 추가하면 재발이 크게 줄어듭니다.
  • 에이전트가 커지면 도구도 많아져서, 배포 후에야 invalid_tool_schema가 터지면 영향이 큽니다. 쿠버네티스/배포 파이프라인에서 문제가 생겼을 때처럼(예: K8s CrashLoopBackOff - Readiness·Liveness 5분 진단) “최소 재현 → 원인 축소 → 점진적 확장” 루틴을 그대로 적용하는 게 효과적입니다.

마무리

400 invalid_tool_schema는 대부분 “JSON Schema를 조금이라도 엄격하게 쓰려다 생기는 사소한 불일치”에서 시작합니다. 최상위 object, required/properties 정합성, additionalProperties, null 처리, 배열(items) 규칙만 체계적으로 점검하면 빠르게 해결됩니다.

다음 단계로는, 스키마를 통과시킨 뒤에도 모델이 엉뚱한 인자를 넣거나 호출을 망설이는 문제가 생길 수 있으니 description을 더 명확히 쓰고, 입력 예시를 시스템 프롬프트/가이드에 포함하는 방식으로 안정성을 높여보는 것을 권합니다.