스키마 정규화
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)
- McpToolset는 도구를 검색하고 원시
inputSchema를 수정 없이 저장합니다. - 모델이 요청을 구성할 때 각 도구의 스키마에 대해
schema_adapter().normalize_schema()를 호출합니다. - 각 제공업체에는 해당 API에 필요한 변환만 적용하는 자체 어댑터가 있습니다.
- 중복 정규화를 방지하기 위해 결과는 콘텐츠 해시를 기준으로 캐시됩니다.
제공업체별 동작
| 기능 | Gemini | OpenAI Strict | OpenAI | Anthropic | Generic |
|---|---|---|---|---|---|
$ref 해상도 | ✅ 인라인 처리 | ❌ 보존 | ❌ 보존 | ❌ 보존 | ❌ 보존 |
anyOf/oneOf | 병합 | 보존 | 보존 | 보존 | 보존 |
allOf | 병합 | 유지 | 유지 | 유지 | 유지 |
additionalProperties | 제거 | false 설정 | 유지 | 유지 | 유지 |
| 타입 배열 | 축소 | 유지 | 유지 | 유지 | 유지 |
$schema | 제거 | 제거 | 제거 | 제거 | 제거 |
if/then/else | 제거 | 제거 | 제거 | 제거 | 제거 |
const | → enum | → enum | → enum | 보존 | → 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();
변환 파이프라인:
$ref확인 (정의에서 인라인 처리하고 깊이 10에서 순환 중단)$schema제거anyOf/oneOf축소 → null이 아닌 첫 번째 하위 스키마allOf하위 스키마 병합- 타입 배열 축소 (
["string", "null"]→"string") if/then/else제거const를 단일 요소enum로 변환enum배열에서 null 제거- 암시적
type: "object"추가 - 지원되지 않는 키워드 제거
- 지원되지 않는
format값 제거 - 중첩 깊이 적용 (5개 수준)
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_keywords—if/then/else제거add_implicit_object_type—properties이 존재할 때type: "object"추가convert_const_to_enum—const를 단일 요소enum로 변환strip_unsupported_formats— 허용 목록에 없는 형식 값을 제거strip_null_from_enum— 열거형 배열에서 null 제거truncate_tool_name— UTF-8 경계에서 잘라냄resolve_refs— 정의에서$ref참조를 인라인 처리collapse_combiners—anyOf/oneOf를 첫 번째 null이 아닌 값으로 축약merge_all_of—allOf하위 스키마 병합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를 사용하여 직접 정규화합니다.