0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GoでstructからJSON Schemaを生成してバリデーションする方法

Posted at

背景

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定義方法

例えば、enumrequiredなどのタグは下記のように付与することができます。

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にJSONSchemaIfJSONSchemaThenなどのメソッドを実装することで、表現することができます。

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ライブラリのものになるため、もう少し具体的な実装例を見たい方は下記のコードを参考にしてみてください。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?