Go SDK: Align hooks with SEP (#23)

The SEP spec and C# SDK use hooks (plural, JSON array) but the Go SDK
had hook (singular, single object). This meant the wire formats were
incompatible — Go would send `{"hook": {...}}` while everything else
expects `{"hooks": [...]}`.

Signed-off-by: Ukjae Jeong <jeongukjae@gmail.com>
This commit is contained in:
Ukjae Jeong 2026-06-16 15:49:49 +01:00 • committed by GitHub
commit 23d889254b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 110 additions and 92 deletions

View file

@ -20,10 +20,10 @@ ext := extension.New()
ext.AddInterceptor(&interceptors.Validator{
Metadata: interceptors.Metadata{
Name: "block-dangerous",
Hook: interceptors.Hook{
Hooks: []interceptors.Hook{{
Events: []string{interceptors.EventToolsCall},
Phase: interceptors.PhaseRequest,
},
}},
Mode: interceptors.ModeEnforce,
},
Handler: func(_ context.Context, inv *interceptors.Invocation) (*interceptors.ValidationResult, error) {

View file

@ -37,10 +37,10 @@ func main() {
m := &interceptors.Mutator{
Metadata: interceptors.Metadata{
Name: "audit-tag",
Hook: interceptors.Hook{
Hooks: []interceptors.Hook{{
Events: []string{interceptors.EventToolsCall},
Phase: interceptors.PhaseResponse,
},
}},
Mode: interceptors.ModeEnforce,
},
Handler: func(_ context.Context, inv *interceptors.Invocation) (*interceptors.MutationResult, error) {

View file

@ -37,10 +37,10 @@ func main() {
v := &interceptors.Validator{
Metadata: interceptors.Metadata{
Name: "block-dangerous-tool",
Hook: interceptors.Hook{
Hooks: []interceptors.Hook{{
Events: []string{interceptors.EventToolsCall},
Phase: interceptors.PhaseRequest,
},
}},
Mode: interceptors.ModeEnforce,
},
Handler: func(_ context.Context, inv *interceptors.Invocation) (*interceptors.ValidationResult, error) {

View file

@ -143,10 +143,7 @@ func (c *Chain) Execute(ctx context.Context, params *ExecutionParams) (*Executio
if len(nameFilter) > 0 && !nameFilter[e.Interceptor.Name] {
continue
}
if !matchesPhase(e.Interceptor.Hook.Phase, params.Phase) {
continue
}
if !slices.Contains(e.Interceptor.Hook.Events, params.Event) {
if !matchesHooks(e.Interceptor.Hooks, params.Event, params.Phase) {
continue
}
switch e.Interceptor.Type {
@ -432,8 +429,16 @@ func (c *Chain) timeoutResult(cr *ExecutionResult, start time.Time) {
})
}
// matchesPhase checks if an interceptor's configured phase covers the target
// phase. An interceptor with PhaseBoth matches any target phase.
func matchesPhase(interceptorPhase, targetPhase interceptors.InterceptionPhase) bool {
return interceptorPhase == interceptors.PhaseBoth || interceptorPhase == targetPhase
// matchesHooks checks if any hook entry matches both the target event and phase.
// A hook with PhaseBoth matches any target phase.
func matchesHooks(hooks []interceptors.Hook, event string, phase interceptors.InterceptionPhase) bool {
for _, h := range hooks {
if h.Phase != interceptors.PhaseBoth && h.Phase != phase {
continue
}
if slices.Contains(h.Events, event) {
return true
}
}
return false
}

View file

@ -67,10 +67,12 @@ func registerInterceptorMethods(server *mcp.Server, is []interceptors.Intercepto
for _, i := range is {
if event != "" {
match := false
for _, e := range i.GetMetadata().Hook.Events {
if e == event {
match = true
break
for _, h := range i.GetMetadata().Hooks {
for _, e := range h.Events {
if e == event {
match = true
break
}
}
}
if !match {
@ -142,10 +144,10 @@ func TestChain_FailOpenRecordsExecutionResult(t *testing.T) {
failOpenValidator := &interceptors.Validator{
Metadata: interceptors.Metadata{
Name: "fo-validator",
Hook: interceptors.Hook{
Hooks: []interceptors.Hook{{
Events: []string{"test/event"},
Phase: interceptors.PhaseRequest,
},
}},
Mode: interceptors.ModeEnforce,
FailOpen: true,
},
@ -156,10 +158,10 @@ func TestChain_FailOpenRecordsExecutionResult(t *testing.T) {
passingValidator := &interceptors.Validator{
Metadata: interceptors.Metadata{
Name: "passing-validator",
Hook: interceptors.Hook{
Hooks: []interceptors.Hook{{
Events: []string{"test/event"},
Phase: interceptors.PhaseRequest,
},
}},
Mode: interceptors.ModeEnforce,
},
Handler: func(_ context.Context, _ *interceptors.Invocation) (*interceptors.ValidationResult, error) {
@ -190,10 +192,10 @@ func TestChain_FailOpenRecordsExecutionResult(t *testing.T) {
failOpenMutator := &interceptors.Mutator{
Metadata: interceptors.Metadata{
Name: "fo-mutator",
Hook: interceptors.Hook{
Hooks: []interceptors.Hook{{
Events: []string{"test/event"},
Phase: interceptors.PhaseResponse,
},
}},
Mode: interceptors.ModeEnforce,
FailOpen: true,
PriorityHint: interceptors.NewPriority(10),
@ -205,10 +207,10 @@ func TestChain_FailOpenRecordsExecutionResult(t *testing.T) {
passingMutator := &interceptors.Mutator{
Metadata: interceptors.Metadata{
Name: "passing-mutator",
Hook: interceptors.Hook{
Hooks: []interceptors.Hook{{
Events: []string{"test/event"},
Phase: interceptors.PhaseResponse,
},
}},
Mode: interceptors.ModeEnforce,
PriorityHint: interceptors.NewPriority(20),
},

View file

@ -45,10 +45,10 @@
// v := &interceptors.Validator{
// Metadata: interceptors.Metadata{
// Name: "block-dangerous-tool",
// Hook: interceptors.Hook{
// Hooks: []interceptors.Hook{{
// Events: []string{"tools/call"},
// Phase: interceptors.PhaseRequest,
// },
// }},
// Mode: interceptors.ModeEnforce,
// },
// Handler: func(ctx context.Context, inv *interceptors.Invocation) (*interceptors.ValidationResult, error) {
@ -73,10 +73,10 @@
// m := &interceptors.Mutator{
// Metadata: interceptors.Metadata{
// Name: "redact-pii",
// Hook: interceptors.Hook{
// Hooks: []interceptors.Hook{{
// Events: []string{"tools/call"},
// Phase: interceptors.PhaseResponse,
// },
// }},
// Mode: interceptors.ModeEnforce,
// },
// Handler: func(ctx context.Context, inv *interceptors.Invocation) (*interceptors.MutationResult, error) {

View file

@ -27,7 +27,7 @@ func (e *Extension) handleList(_ context.Context, req *mcp.ServerRequest[*interc
all := e.getInterceptors()
infos := make([]interceptors.InterceptorInfo, 0, len(all))
for _, ri := range all {
if event != "" && !slices.Contains(ri.GetMetadata().Hook.Events, event) {
if event != "" && !hooksContainEvent(ri.GetMetadata().Hooks, event) {
continue
}
infos = append(infos, interceptors.InfoFromInterceptor(ri))
@ -105,3 +105,12 @@ func (e *Extension) handleInvoke(ctx context.Context, req *mcp.ServerRequest[*in
return result, nil
}
func hooksContainEvent(hooks []interceptors.Hook, event string) bool {
for _, h := range hooks {
if slices.Contains(h.Events, event) {
return true
}
}
return false
}

View file

@ -42,10 +42,10 @@ func TestListWithEventFilter(t *testing.T) {
promptsValidator := &interceptors.Validator{
Metadata: interceptors.Metadata{
Name: "prompts-v",
Hook: interceptors.Hook{
Hooks: []interceptors.Hook{{
Events: []string{interceptors.EventPromptsGet},
Phase: interceptors.PhaseRequest,
},
}},
Mode: interceptors.ModeEnforce,
},
Handler: func(_ context.Context, _ *interceptors.Invocation) (*interceptors.ValidationResult, error) {
@ -112,10 +112,10 @@ func TestInvokeValidatorRejects(t *testing.T) {
rejectAll := &interceptors.Validator{
Metadata: interceptors.Metadata{
Name: "reject-all",
Hook: interceptors.Hook{
Hooks: []interceptors.Hook{{
Events: []string{interceptors.EventToolsCall},
Phase: interceptors.PhaseRequest,
},
}},
Mode: interceptors.ModeEnforce,
},
Handler: func(_ context.Context, _ *interceptors.Invocation) (*interceptors.ValidationResult, error) {
@ -155,10 +155,10 @@ func TestInvokeMutator(t *testing.T) {
mutator := &interceptors.Mutator{
Metadata: interceptors.Metadata{
Name: "add-field",
Hook: interceptors.Hook{
Hooks: []interceptors.Hook{{
Events: []string{interceptors.EventToolsCall},
Phase: interceptors.PhaseRequest,
},
}},
Mode: interceptors.ModeEnforce,
},
Handler: func(_ context.Context, inv *interceptors.Invocation) (*interceptors.MutationResult, error) {
@ -231,10 +231,10 @@ func TestInvokeTimeout(t *testing.T) {
slowValidator := &interceptors.Validator{
Metadata: interceptors.Metadata{
Name: "slow-v",
Hook: interceptors.Hook{
Hooks: []interceptors.Hook{{
Events: []string{interceptors.EventToolsCall},
Phase: interceptors.PhaseRequest,
},
}},
Mode: interceptors.ModeEnforce,
},
Handler: func(ctx context.Context, _ *interceptors.Invocation) (*interceptors.ValidationResult, error) {

View file

@ -181,8 +181,10 @@ func (e *Extension) enrichInitResult(result mcp.Result) (mcp.Result, error) {
all := e.getInterceptors()
supportedEvents := map[string]bool{}
for _, ri := range all {
for _, ev := range ri.GetMetadata().Hook.Events {
supportedEvents[ev] = true
for _, h := range ri.GetMetadata().Hooks {
for _, ev := range h.Events {
supportedEvents[ev] = true
}
}
}

View file

@ -86,10 +86,10 @@ func TestServer_ValidatorWarnDoesNotBlock(t *testing.T) {
warnValidator := &interceptors.Validator{
Metadata: interceptors.Metadata{
Name: "warn-only",
Hook: interceptors.Hook{
Hooks: []interceptors.Hook{{
Events: []string{interceptors.EventToolsCall},
Phase: interceptors.PhaseRequest,
},
}},
Mode: interceptors.ModeEnforce,
},
Handler: func(_ context.Context, _ *interceptors.Invocation) (*interceptors.ValidationResult, error) {
@ -117,10 +117,10 @@ func TestServer_ValidationFailurePreventsM(t *testing.T) {
spy := &interceptors.Mutator{
Metadata: interceptors.Metadata{
Name: "spy-mutator",
Hook: interceptors.Hook{
Hooks: []interceptors.Hook{{
Events: []string{interceptors.EventToolsCall},
Phase: interceptors.PhaseRequest,
},
}},
Mode: interceptors.ModeEnforce,
},
Handler: func(_ context.Context, _ *interceptors.Invocation) (*interceptors.MutationResult, error) {
@ -141,10 +141,10 @@ func TestServer_ValidatorTimeoutAbortsChain(t *testing.T) {
slowValidator := &interceptors.Validator{
Metadata: interceptors.Metadata{
Name: "slow-validator",
Hook: interceptors.Hook{
Hooks: []interceptors.Hook{{
Events: []string{interceptors.EventToolsCall},
Phase: interceptors.PhaseRequest,
},
}},
Mode: interceptors.ModeEnforce,
},
Handler: func(ctx context.Context, _ *interceptors.Invocation) (*interceptors.ValidationResult, error) {
@ -172,10 +172,10 @@ func TestServer_MutatorTimeoutAbortsChain(t *testing.T) {
slowMutator := &interceptors.Mutator{
Metadata: interceptors.Metadata{
Name: "m2-slow",
Hook: interceptors.Hook{
Hooks: []interceptors.Hook{{
Events: []string{interceptors.EventToolsCall},
Phase: interceptors.PhaseResponse,
},
}},
Mode: interceptors.ModeEnforce,
PriorityHint: interceptors.NewPriority(20),
},
@ -208,10 +208,10 @@ func TestServer_MutatorFailOpenContinuesChain(t *testing.T) {
failOpenMutator := &interceptors.Mutator{
Metadata: interceptors.Metadata{
Name: "m2-failopen",
Hook: interceptors.Hook{
Hooks: []interceptors.Hook{{
Events: []string{interceptors.EventToolsCall},
Phase: interceptors.PhaseResponse,
},
}},
Mode: interceptors.ModeEnforce,
FailOpen: true,
PriorityHint: interceptors.NewPriority(20),
@ -248,10 +248,10 @@ func TestServer_ResponseMutatorSeesMutatedParams(t *testing.T) {
reqMutator := &interceptors.Mutator{
Metadata: interceptors.Metadata{
Name: "inject-marker",
Hook: interceptors.Hook{
Hooks: []interceptors.Hook{{
Events: []string{interceptors.EventToolsCall},
Phase: interceptors.PhaseRequest,
},
}},
Mode: interceptors.ModeEnforce,
},
Handler: func(_ context.Context, inv *interceptors.Invocation) (*interceptors.MutationResult, error) {
@ -309,10 +309,10 @@ func TestServer_CombinedValidatorsAndMutators(t *testing.T) {
reqV1 := &interceptors.Validator{
Metadata: interceptors.Metadata{
Name: "req-v1-tool-check",
Hook: interceptors.Hook{
Hooks: []interceptors.Hook{{
Events: []string{interceptors.EventToolsCall},
Phase: interceptors.PhaseRequest,
},
}},
Mode: interceptors.ModeEnforce,
},
Handler: func(_ context.Context, inv *interceptors.Invocation) (*interceptors.ValidationResult, error) {
@ -343,10 +343,10 @@ func TestServer_CombinedValidatorsAndMutators(t *testing.T) {
reqV2 := &interceptors.Validator{
Metadata: interceptors.Metadata{
Name: "req-v2-args-check",
Hook: interceptors.Hook{
Hooks: []interceptors.Hook{{
Events: []string{interceptors.EventToolsCall},
Phase: interceptors.PhaseRequest,
},
}},
Mode: interceptors.ModeEnforce,
},
Handler: func(_ context.Context, inv *interceptors.Invocation) (*interceptors.ValidationResult, error) {
@ -381,10 +381,10 @@ func TestServer_CombinedValidatorsAndMutators(t *testing.T) {
reqV3 := &interceptors.Validator{
Metadata: interceptors.Metadata{
Name: "req-v3-warn",
Hook: interceptors.Hook{
Hooks: []interceptors.Hook{{
Events: []string{interceptors.EventToolsCall},
Phase: interceptors.PhaseRequest,
},
}},
Mode: interceptors.ModeEnforce,
},
Handler: func(_ context.Context, _ *interceptors.Invocation) (*interceptors.ValidationResult, error) {
@ -405,10 +405,10 @@ func TestServer_CombinedValidatorsAndMutators(t *testing.T) {
return &interceptors.Mutator{
Metadata: interceptors.Metadata{
Name: name,
Hook: interceptors.Hook{
Hooks: []interceptors.Hook{{
Events: []string{interceptors.EventToolsCall},
Phase: interceptors.PhaseRequest,
},
}},
Mode: interceptors.ModeEnforce,
PriorityHint: interceptors.NewPriority(priority),
},
@ -454,10 +454,10 @@ func TestServer_CombinedValidatorsAndMutators(t *testing.T) {
return &interceptors.Mutator{
Metadata: interceptors.Metadata{
Name: name,
Hook: interceptors.Hook{
Hooks: []interceptors.Hook{{
Events: []string{interceptors.EventToolsCall},
Phase: interceptors.PhaseResponse,
},
}},
Mode: interceptors.ModeEnforce,
PriorityHint: interceptors.NewPriority(priority),
},
@ -496,10 +496,10 @@ func TestServer_CombinedValidatorsAndMutators(t *testing.T) {
respV1 := &interceptors.Validator{
Metadata: interceptors.Metadata{
Name: "resp-v1-has-content",
Hook: interceptors.Hook{
Hooks: []interceptors.Hook{{
Events: []string{interceptors.EventToolsCall},
Phase: interceptors.PhaseResponse,
},
}},
Mode: interceptors.ModeEnforce,
},
Handler: func(_ context.Context, inv *interceptors.Invocation) (*interceptors.ValidationResult, error) {
@ -530,10 +530,10 @@ func TestServer_CombinedValidatorsAndMutators(t *testing.T) {
respV2 := &interceptors.Validator{
Metadata: interceptors.Metadata{
Name: "resp-v2-sees-mutation",
Hook: interceptors.Hook{
Hooks: []interceptors.Hook{{
Events: []string{interceptors.EventToolsCall},
Phase: interceptors.PhaseResponse,
},
}},
Mode: interceptors.ModeEnforce,
},
Handler: func(_ context.Context, inv *interceptors.Invocation) (*interceptors.ValidationResult, error) {
@ -566,10 +566,10 @@ func TestServer_CombinedValidatorsAndMutators(t *testing.T) {
respV3 := &interceptors.Validator{
Metadata: interceptors.Metadata{
Name: "resp-v3-info",
Hook: interceptors.Hook{
Hooks: []interceptors.Hook{{
Events: []string{interceptors.EventToolsCall},
Phase: interceptors.PhaseResponse,
},
}},
Mode: interceptors.ModeEnforce,
},
Handler: func(_ context.Context, _ *interceptors.Invocation) (*interceptors.ValidationResult, error) {

View file

@ -190,10 +190,10 @@ func prefixMutator(name, tag string, phase interceptors.InterceptionPhase, prior
return &interceptors.Mutator{
Metadata: interceptors.Metadata{
Name: name,
Hook: interceptors.Hook{
Hooks: []interceptors.Hook{{
Events: []string{interceptors.EventToolsCall},
Phase: phase,
},
}},
Mode: mode,
PriorityHint: interceptors.NewPriority(priority),
},
@ -225,10 +225,10 @@ func failMutator(name string, phase interceptors.InterceptionPhase, priority int
return &interceptors.Mutator{
Metadata: interceptors.Metadata{
Name: name,
Hook: interceptors.Hook{
Hooks: []interceptors.Hook{{
Events: []string{interceptors.EventToolsCall},
Phase: phase,
},
}},
Mode: interceptors.ModeEnforce,
PriorityHint: interceptors.NewPriority(priority),
},
@ -243,10 +243,10 @@ func blockToolValidator(toolName string) *interceptors.Validator {
return &interceptors.Validator{
Metadata: interceptors.Metadata{
Name: "block-" + toolName,
Hook: interceptors.Hook{
Hooks: []interceptors.Hook{{
Events: []string{interceptors.EventToolsCall},
Phase: interceptors.PhaseRequest,
},
}},
Mode: interceptors.ModeEnforce,
},
Handler: func(_ context.Context, inv *interceptors.Invocation) (*interceptors.ValidationResult, error) {
@ -278,10 +278,10 @@ func allowAllValidator(name string) *interceptors.Validator {
return &interceptors.Validator{
Metadata: interceptors.Metadata{
Name: name,
Hook: interceptors.Hook{
Hooks: []interceptors.Hook{{
Events: []string{interceptors.EventToolsCall},
Phase: interceptors.PhaseRequest,
},
}},
Mode: interceptors.ModeEnforce,
},
Handler: func(_ context.Context, _ *interceptors.Invocation) (*interceptors.ValidationResult, error) {

View file

@ -81,10 +81,10 @@ func TestMiddlewareBlocksOnValidationError(t *testing.T) {
blocker := &interceptors.Validator{
Metadata: interceptors.Metadata{
Name: "block-echo",
Hook: interceptors.Hook{
Hooks: []interceptors.Hook{{
Events: []string{interceptors.EventToolsCall},
Phase: interceptors.PhaseRequest,
},
}},
Mode: interceptors.ModeEnforce,
},
Handler: func(_ context.Context, inv *interceptors.Invocation) (*interceptors.ValidationResult, error) {
@ -125,10 +125,10 @@ func TestMiddlewarePassesOnSuccess(t *testing.T) {
allow := &interceptors.Validator{
Metadata: interceptors.Metadata{
Name: "allow-all",
Hook: interceptors.Hook{
Hooks: []interceptors.Hook{{
Events: []string{interceptors.EventToolsCall},
Phase: interceptors.PhaseRequest,
},
}},
Mode: interceptors.ModeEnforce,
},
Handler: func(_ context.Context, _ *interceptors.Invocation) (*interceptors.ValidationResult, error) {
@ -152,10 +152,10 @@ func TestMiddlewareMutatesPayload(t *testing.T) {
mutator := &interceptors.Mutator{
Metadata: interceptors.Metadata{
Name: "add-prefix",
Hook: interceptors.Hook{
Hooks: []interceptors.Hook{{
Events: []string{interceptors.EventToolsCall},
Phase: interceptors.PhaseResponse,
},
}},
Mode: interceptors.ModeEnforce,
},
Handler: func(_ context.Context, inv *interceptors.Invocation) (*interceptors.MutationResult, error) {
@ -197,10 +197,10 @@ func TestMiddlewareAuditModeNonBlocking(t *testing.T) {
auditValidator := &interceptors.Validator{
Metadata: interceptors.Metadata{
Name: "audit-reject",
Hook: interceptors.Hook{
Hooks: []interceptors.Hook{{
Events: []string{interceptors.EventToolsCall},
Phase: interceptors.PhaseRequest,
},
}},
Mode: interceptors.ModeAudit,
},
Handler: func(_ context.Context, _ *interceptors.Invocation) (*interceptors.ValidationResult, error) {
@ -230,10 +230,10 @@ func TestMiddlewareWithContextProvider(t *testing.T) {
principalCheck := &interceptors.Validator{
Metadata: interceptors.Metadata{
Name: "check-principal",
Hook: interceptors.Hook{
Hooks: []interceptors.Hook{{
Events: []string{interceptors.EventToolsCall},
Phase: interceptors.PhaseRequest,
},
}},
Mode: interceptors.ModeEnforce,
},
Handler: func(_ context.Context, inv *interceptors.Invocation) (*interceptors.ValidationResult, error) {
@ -312,10 +312,10 @@ func TestMiddlewareRequestMutatorModifiesArgs(t *testing.T) {
reqMutator := &interceptors.Mutator{
Metadata: interceptors.Metadata{
Name: "inject-field",
Hook: interceptors.Hook{
Hooks: []interceptors.Hook{{
Events: []string{interceptors.EventToolsCall},
Phase: interceptors.PhaseRequest,
},
}},
Mode: interceptors.ModeEnforce,
},
Handler: func(_ context.Context, inv *interceptors.Invocation) (*interceptors.MutationResult, error) {

View file

@ -74,7 +74,7 @@ type Metadata struct {
Version string `json:"version,omitempty"`
Description string `json:"description,omitempty"`
Type InterceptorType `json:"type"`
Hook Hook `json:"hook"`
Hooks []Hook `json:"hooks"`
PriorityHint Priority `json:"priorityHint,omitempty"`
Compat *Compat `json:"compat,omitempty"`
ConfigSchema json.RawMessage `json:"configSchema,omitempty"`

View file

@ -42,7 +42,7 @@ type InterceptorInfo struct {
Version string `json:"version,omitempty"`
Description string `json:"description,omitempty"`
Type InterceptorType `json:"type"`
Hook Hook `json:"hook"`
Hooks []Hook `json:"hooks"`
PriorityHint Priority `json:"priorityHint,omitempty"`
Compat *Compat `json:"compat,omitempty"`
ConfigSchema json.RawMessage `json:"configSchema,omitempty"`
@ -58,7 +58,7 @@ func InfoFromInterceptor(i Interceptor) InterceptorInfo {
Version: m.Version,
Description: m.Description,
Type: i.GetType(),
Hook: m.Hook,
Hooks: m.Hooks,
PriorityHint: m.PriorityHint,
Compat: m.Compat,
ConfigSchema: m.ConfigSchema,