背景
MCPサーバのToolが使用される際に、MCPクライアントから送られてくるパラメータについてはLLMが何を考えているのかわからないように、何が送られてくるかはわかりません。
しかし流石にデタラメなものが送られてきては困るので、ToolにはinputShema
というJSON Schemaを返すレスポンスがあり、そこでパラメータの定義を行っています。
ただしあくまでMCPクライアントにパラメーターの定義を送っているのみで、現状MCPサーバのSDKでinputSchema
のJSON Schemaを元にバリデーションしていないものが多そうです。
また人間が不正なパラメータを送るように指示してしまえば、MCPクライアントは人間の言う事に従って不正なパラメータを送ってきます。
そのため、安全にするためにはToolの実装側でJSON Schemaを元にバリデーションを行う必要があります。
単純なバリデーションであればよいですが、If-Then-Elseなどを使用したバリデーションをJSON Schemaで定義をしているのに、別でバリデーション処理を実装するのは少し面倒です。
そこで2つのOSSを使用させていただいて、GoのstructからJSON Schemaを生成してバリデーションを行う方法を実際に試してうまく行ったので紹介します。
structからJSON Schemaを生成する方法
今回使用したライブラリは以下になります。
このライブラリではstructのフィールドにタグを付与することで、JSON Schemaを生成することができます。
structでのJSON Schema定義方法
例えば、enum
やrequired
などのタグは下記のように付与することができます。
type BotDeviceCommandParameter struct {
Command string `json:"command" title:"Command" enum:"TurnOn,TurnOff,Press" description:"TurnOn:set to OFF state, TurnOff:set to ON state, Press:trigger press" required:"true"`
_ struct{} `additionalProperties:"false"`
}
またIf-Then-Elseのような条件を定義したい場合は、下記のようにstructにJSONSchemaIf
やJSONSchemaThen
などのメソッドを実装することで、表現することができます。
type CurtainDeviceCommandParameter struct {
Command string `json:"command" title:"Command" enum:"TurnOn,TurnOff,Pause,SetPosition" description:"TurnOn:equivalent to set position to 100, TurnOff:equivalent to set position to 0, Pause:set to PAUSE state, SetPosition:set position" required:"true"`
Mode string `json:"mode" title:"Mode" enum:"0,1,ff" description:"0:performance mode, 1:silent mode, ff:default mode"`
Position int `json:"position" title:"Position" minimum:"0" maximum:"100"`
_ struct{} `additionalProperties:"false"`
}
func (parameter *CurtainDeviceCommandParameter) JSONSchemaIf() interface{} {
return struct {
Command string `json:"command" const:"SetPosition" required:"true"`
}{}
}
func (parameter *CurtainDeviceCommandParameter) JSONSchemaThen() interface{} {
return struct {
Mode string `json:"mode" required:"true"`
Position int `json:"position" required:"true"`
}{}
}
更に複数の条件があり、allOf
を使用したい場合はJSONSchemaAllOf
メソッドを実装して下記のように表現ができます。
type CeilingLightDeviceCommandParameter struct {
Command string `json:"command" title:"Command" enum:"TurnOn,TurnOff,Toggle,SetBrightness,SetColorTemperature" description:"TurnOn:turn on the ceiling light, TurnOff:turn off the ceiling light, Toggle:toggle the ceiling light, SetBrightness:set brightness, SetColorTemperature:set color temperature" required:"true"`
Brightness int `json:"brightness" title:"Brightness" minimum:"1" maximum:"100" description:"Brightness level (1-100)"`
ColorTemperature int `json:"colorTemperature" title:"ColorTemperature" minimum:"2700" maximum:"6500" description:"Color temperature in Kelvin (2700-6500)"`
_ struct{} `additionalProperties:"false"`
}
type CeilingLightDeviceCommandSetBrightnessIfExposer struct{}
func (parameter *CeilingLightDeviceCommandSetBrightnessIfExposer) JSONSchemaIf() interface{} {
return struct {
Command string `json:"command" const:"SetBrightness" required:"true"`
}{}
}
func (parameter *CeilingLightDeviceCommandSetBrightnessIfExposer) JSONSchemaThen() interface{} {
return struct {
Brightness int `json:"brightness" required:"true"`
}{}
}
type CeilingLightDeviceCommandSetColorTemperatureIfExposer struct{}
func (parameter *CeilingLightDeviceCommandSetColorTemperatureIfExposer) JSONSchemaIf() interface{} {
return struct {
Command string `json:"command" const:"SetColorTemperature" required:"true"`
}{}
}
func (parameter *CeilingLightDeviceCommandSetColorTemperatureIfExposer) JSONSchemaThen() interface{} {
return struct {
ColorTemperature int `json:"colorTemperature" required:"true"`
}{}
}
func (parameter *CeilingLightDeviceCommandParameter) JSONSchemaAllOf() []interface{} {
return []interface{}{
&CeilingLightDeviceCommandSetBrightnessIfExposer{},
&CeilingLightDeviceCommandSetColorTemperatureIfExposer{},
}
}
JSON Schemaの文字列での取得方法
JSON Schemaの定義をしたstructを文字列に変換する場合は下記のようなコードで文字列に変換することができます。
reflector := jsonschema.Reflector{}
// MEMO: $refを使用させないためにInlineRefsを指定しています
schema, err := reflector.Reflect(CeilingLightDeviceCommandParameter{}, jsonschema.InlineRefs)
if err != nil {
return "", err
}
jsonString, err := json.Marshal(schema)
if err != nil {
return "", err
}
return string(jsonString), nil
JSON Schemaを元にバリデーションする方法
今回使用したライブラリは以下になります。
このライブラリでは、下記のようにJSON Schemaを元にバリデーションを行うことができます。
func validateAndUnmarshalJSON(device ExecutableCommandDevice, jsonString string, target interface{}) error {
schemaJSON, err := device.GetCommandParameterJSONSchema()
if err != nil {
return err
}
compiler := jsonschemaValidation.NewCompiler()
schema, err := compiler.Compile([]byte(schemaJSON))
if err != nil {
return err
}
var instance map[string]interface{}
err = json.Unmarshal([]byte(jsonString), &instance)
if err != nil {
return err
}
result := schema.Validate(instance)
if !result.IsValid() {
errorDetails, _ := json.Marshal(result.ToList())
return fmt.Errorf("invalid command parameter: %s", string(errorDetails))
}
return json.Unmarshal([]byte(jsonString), target)
}
structからJSON Schemaを生成するためのライブラリと名前が被っているため、jsonschemaValidation "github.com/kaptinlin/jsonschema"
と別名でインポートしているのに注意してください。
GetCommandParameterJSONSchema
メソッドでJSON Schemaを文字列で取得しています。
また、検証対象のJSONはjsonString
で受け取っています。
最後に
これら2つのライブラリを使用することでGoのstructでJSON Schemaを定義して、更にそれを使いまわしてバリデーションするということができるようになりました。
ただしSDKの実装の都合などもあり、実際にはMCPサーバのinputShema
のレスポンスに直接今回の仕組みを使っているわけではなく、パラメータとして文字列を受け取るようにしてそこで今回の仕組みを使っています。
今回のサンプルコードはSwitchBotのMCPサーバで使用しているAPIライブラリのものになるため、もう少し具体的な実装例を見たい方は下記のコードを参考にしてみてください。