스키마 정규화

ADK-Rust는 요청 시 각 LLM 제공업체에 맞게 MCP 도구 스키마를 자동으로 정규화합니다. 따라서 MCP 도구는 수동으로 스키마를 조정하지 않아도 Gemini, OpenAI, Anthropic 및 기타 제공업체에서 원활하게 작동합니다.

작동 방식

MCP Server → raw JSON Schema → McpToolset (stores verbatim)
                                      ↓
                          Model.generate_content()
                                      ↓
                          SchemaAdapter.normalize_schema()
                                      ↓
                          Provider API (Gemini/OpenAI/Anthropic)
  1. McpToolset는 도구를 검색하고 원시 inputSchema를 수정 없이 저장합니다.
  2. 모델이 요청을 구성할 때 각 도구의 스키마에 대해 schema_adapter().normalize_schema()를 호출합니다.
  3. 각 제공업체에는 해당 API에 필요한 변환만 적용하는 자체 어댑터가 있습니다.
  4. 중복 정규화를 방지하기 위해 결과는 콘텐츠 해시를 기준으로 캐시됩니다.

제공업체별 동작

기능GeminiOpenAI StrictOpenAIAnthropicGeneric
$ref 해상도✅ 인라인 처리❌ 보존❌ 보존❌ 보존❌ 보존
anyOf/oneOf병합보존보존보존보존
allOf병합유지유지유지유지
additionalProperties제거false 설정유지유지유지
타입 배열축소유지유지유지유지
$schema제거제거제거제거제거
if/then/else제거제거제거제거제거
constenumenumenum보존enum
지원되지 않는 format제거제거제거보존제거
중첩 깊이 제한5개 수준없음없음없음없음
exclusiveMin/Max제거유지유지유지유지

SchemaAdapter 트레이트

모든 어댑터는 adk-core의 이 트레이트를 구현합니다:

use serde_json::Value;
use std::borrow::Cow;

pub trait SchemaAdapter: Send + Sync + std::fmt::Debug {
    /// Normalize a raw JSON Schema for this provider.
    fn normalize_schema(&self, schema: Value) -> Value;

    /// Normalize a tool name (default: truncate to 64 bytes at UTF-8 boundary).
    fn normalize_tool_name<'a>(&self, name: &'a str) -> Cow<'a, str>;

    /// Fallback schema when no parameters_schema is provided.
    fn empty_schema(&self) -> Value;
}

Llm 구현은 schema_adapter()를 통해 해당 어댑터를 반환합니다:

use adk_core::Llm;

let model = GeminiModel::new(&api_key, "gemini-3.7-flash")?;
let adapter = model.schema_adapter(); // Returns &GeminiSchemaAdapter

사용 가능한 어댑터

GeminiSchemaAdapter

가장 공격적인 어댑터입니다. Gemini의 함수 호출 API에 필요한 모든 파괴적 변환을 적용합니다:

use adk_gemini::schema_adapter::GeminiSchemaAdapter;
use adk_core::SchemaAdapter;

// Standard Gemini API
let adapter = GeminiSchemaAdapter::new();

// Vertex AI (sets additionalProperties: false instead of removing)
let adapter = GeminiSchemaAdapter::vertex_ai();

변환 파이프라인:

  1. $ref 확인 (정의에서 인라인 처리하고 깊이 10에서 순환 중단)
  2. $schema 제거
  3. anyOf/oneOf 축소 → null이 아닌 첫 번째 하위 스키마
  4. allOf 하위 스키마 병합
  5. 타입 배열 축소 (["string", "null"]"string")
  6. if/then/else 제거
  7. const를 단일 요소 enum로 변환
  8. enum 배열에서 null 제거
  9. 암시적 type: "object" 추가
  10. 지원되지 않는 키워드 제거
  11. 지원되지 않는 format 값 제거
  12. 중첩 깊이 적용 (5개 수준)
  13. definitions/$defs 제거

OpenAiStrictSchemaAdapter

구조화된 출력을 위해 additionalProperties: false를 추가하면서 스키마 구조를 보존합니다:

use adk_model::openai::OpenAiStrictSchemaAdapter;
use adk_core::SchemaAdapter;

let adapter = OpenAiStrictSchemaAdapter;

OpenAiSchemaAdapter

비엄격 모드에서 안전을 위한 최소한의 수정만 적용합니다:

use adk_model::openai::OpenAiSchemaAdapter;

let adapter = OpenAiSchemaAdapter;

AnthropicSchemaAdapter

거의 그대로 전달합니다. Anthropic은 대부분의 JSON 스키마 기능을 지원합니다:

use adk_model::anthropic::AnthropicSchemaAdapter;

let adapter = AnthropicSchemaAdapter;

GenericSchemaAdapter

알 수 없는 제공업체(Ollama, DeepSeek 등)의 기본값입니다:

use adk_core::GenericSchemaAdapter;

let adapter = GenericSchemaAdapter;

스키마 캐싱

정규화된 스키마는 중복 계산을 방지하기 위해 콘텐츠 해시를 기준으로 캐시됩니다:

use adk_core::{GenericSchemaAdapter, SchemaCache};
use serde_json::json;
use std::sync::Arc;

let cache = SchemaCache::for_adapter(Arc::new(GenericSchemaAdapter));
let schema = json!({"type": "object", "properties": {"name": {"type": "string"}}});

// First call normalizes and caches
let result = cache.normalize(&schema);

// Subsequent calls return cached result (no re-normalization)
let cached = cache.normalize(&schema);

// Invalidate when tools change
cache.clear();

각 캐시는 하나의 어댑터 인스턴스를 소유하므로 서로 다른 제공업체 또는 어댑터 구성에 대해 정규화된 항목이 충돌할 수 없습니다. 제공업체 클라이언트는 generate_content() 중에 이러한 캐시를 자동으로 사용합니다.

도구 이름 잘라내기

모든 어댑터는 64바이트를 초과하는 도구 이름을 유효한 UTF-8 문자 경계에서 잘라냅니다:

use adk_core::SchemaAdapter;
use adk_gemini::schema_adapter::GeminiSchemaAdapter;

let adapter = GeminiSchemaAdapter::new();

// Short names pass through unchanged
let name = adapter.normalize_tool_name("get_weather");
assert_eq!(name, "get_weather");

// Long names are truncated to 64 bytes
let long = "mcp_server_github_com_organization_repository_pull_request_review_comments";
let truncated = adapter.normalize_tool_name(long);
assert!(truncated.len() <= 64);

// Multi-byte characters are never split
let emoji_name = "🔧_tool_名前が長い";
let result = adapter.normalize_tool_name(emoji_name);
assert!(std::str::from_utf8(result.as_bytes()).is_ok());

공유 유틸리티

adk_core::schema_utils 모듈은 어댑터가 내부적으로 사용하는 조합 가능한 변환 함수를 제공합니다:

use adk_core::schema_utils;
use serde_json::json;

let mut schema = json!({
    "$schema": "http://json-schema.org/draft-07/schema#",
    "type": "object",
    "properties": {
        "status": { "const": "active" },
        "email": { "type": "string", "format": "hostname" }
    },
    "if": { "properties": { "x": { "type": "number" } } },
    "then": { "required": ["x"] }
});

// Apply individual transforms
schema_utils::strip_schema_keyword(&mut schema);
schema_utils::strip_conditional_keywords(&mut schema);
schema_utils::convert_const_to_enum(&mut schema);
schema_utils::strip_unsupported_formats(&mut schema, &["date-time", "email", "uri"]);

사용 가능한 유틸리티:

  • strip_schema_keyword$schema 제거
  • strip_conditional_keywordsif/then/else 제거
  • add_implicit_object_typeproperties이 존재할 때 type: "object" 추가
  • convert_const_to_enumconst를 단일 요소 enum로 변환
  • strip_unsupported_formats — 허용 목록에 없는 형식 값을 제거
  • strip_null_from_enum — 열거형 배열에서 null 제거
  • truncate_tool_name — UTF-8 경계에서 잘라냄
  • resolve_refs — 정의에서 $ref 참조를 인라인 처리
  • collapse_combinersanyOf/oneOf를 첫 번째 null이 아닌 값으로 축약
  • merge_all_ofallOf 하위 스키마 병합
  • collapse_type_arrays["string", "null"]"string"로 축약
  • enforce_nesting_depth — 심층 스키마를 {"type": "object"}로 대체

사용자 지정 어댑터

사용자 지정 provider를 위해 SchemaAdapter를 구현합니다:

use adk_core::{SchemaAdapter, schema_utils};
use serde_json::Value;
use std::borrow::Cow;

#[derive(Debug)]
struct MyProviderAdapter;

impl SchemaAdapter for MyProviderAdapter {
    fn normalize_schema(&self, mut schema: Value) -> Value {
        // Apply only the transforms your provider needs
        schema_utils::strip_schema_keyword(&mut schema);
        schema_utils::strip_conditional_keywords(&mut schema);
        schema_utils::add_implicit_object_type(&mut schema);
        // Keep everything else as-is
        schema
    }
}

예시

모든 어댑터가 작동하는 모습을 확인하려면 스키마 정규화 데모를 실행합니다:

cd examples/schema_normalization
cargo run

API 키는 필요하지 않습니다 — 정규화 로직을 로컬에서 보여 줍니다.

sanitize_schema에서 마이그레이션

이전에 사전 정리된 스키마를 반환하는 McpToolset에 의존했다면:

  • 표준 사용에는 코드 변경이 필요하지 않습니다. Toolset 트레이트 API는 변경되지 않았습니다.
  • 이제 스키마는 도구 등록 시점이 아니라 요청 시점에 모델 어댑터에서 정규화됩니다.
  • parameters_schema()를 호출하고 Gemini 형식의 출력을 예상했다면, 이제 원시 스키마가 반환됩니다. 필요한 경우 적절한 SchemaAdapter를 사용하여 직접 정규화합니다.

이전: ← MCP 도구 | 다음: 세션 →