experimental-ext-interceptors/go/sdk/interceptors/extension/rpc_test.go
monikaicampbell bd57572579
Go SDK: Align mode with SEP (#40)
<!-- Provide a brief summary of your changes -->

## Motivation and Context
The SEP-2624 spec defines interceptor modes as "active" | "audit". This
change aligns the Go SDK's interceptor `Mode` value with the SEP spec.

## How Has This Been Tested?
- go test ./... from go/sdk — all packages pass (chain, extension,
integrations/gomiddleware).
- gofmt -l . — clean.
- go vet ./... — clean.

## Breaking Changes
Yes:
- The exported constant interceptors.ModeEnforce is removed and replaced
by interceptors.ModeActive. Callers referencing ModeEnforce must rename.
- The serialized mode value changes from "enforce" to "active". Any
config expecting "enforce" must be updated.

## Types of changes
<!-- What types of changes does your code introduce? Put an `x` in all
the boxes that apply: -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [x] Breaking change (fix or feature that would cause existing
functionality to change)
- [x] Documentation update

## Checklist
<!-- Go over all the following points, and put an `x` in all the boxes
that apply. -->
- [x] I have read the [MCP
Documentation](https://modelcontextprotocol.io)
- [x] My code follows the repository's style guidelines
- [x] New and existing tests pass locally
- [ ] I have added appropriate error handling
- [x] I have added or updated documentation as needed

## Additional context
<!-- Add any other context, implementation notes, or design decisions
-->

Signed-off-by: monikaicampbell <mcampbell350@bloomberg.net>
2026-08-21 15:56:07 +01:00

274 lines
8.1 KiB
Go

package extension_test
import (
"context"
"encoding/json"
"fmt"
"testing"
"time"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/modelcontextprotocol/ext-interceptors/go/sdk/interceptors"
)
// --- interceptors/list tests ---
func TestListInterceptors(t *testing.T) {
t.Parallel()
cs := setupRPCServer(t,
allowAllValidator("v1"),
blockToolValidator("dangerous"),
)
result, err := mcp.CallCustomMethod[*interceptors.ListParams, *interceptors.ListResult](
context.Background(), cs, interceptors.MethodList, nil,
)
require.NoError(t, err)
assert.Len(t, result.Interceptors, 2)
names := make([]string, len(result.Interceptors))
for i, info := range result.Interceptors {
names[i] = info.Name
}
assert.Contains(t, names, "v1")
assert.Contains(t, names, "block-dangerous")
}
func TestListWithEventFilter(t *testing.T) {
t.Parallel()
toolsValidator := allowAllValidator("tools-v")
promptsValidator := &interceptors.Validator{
Metadata: interceptors.Metadata{
Name: "prompts-v",
Hooks: []interceptors.Hook{{
Events: []string{interceptors.EventPromptsGet},
Phase: interceptors.PhaseRequest,
}},
Mode: interceptors.ModeActive,
},
Handler: func(_ context.Context, _ *interceptors.Invocation) (*interceptors.ValidationResult, error) {
return &interceptors.ValidationResult{Valid: true}, nil
},
}
cs := setupRPCServer(t, toolsValidator, promptsValidator)
// Filter for tools/call — should only return tools-v
result, err := mcp.CallCustomMethod[*interceptors.ListParams, *interceptors.ListResult](
context.Background(), cs, interceptors.MethodList,
&interceptors.ListParams{Event: interceptors.EventToolsCall},
)
require.NoError(t, err)
assert.Len(t, result.Interceptors, 1)
assert.Equal(t, "tools-v", result.Interceptors[0].Name)
// Filter for prompts/get — should only return prompts-v
result2, err := mcp.CallCustomMethod[*interceptors.ListParams, *interceptors.ListResult](
context.Background(), cs, interceptors.MethodList,
&interceptors.ListParams{Event: interceptors.EventPromptsGet},
)
require.NoError(t, err)
assert.Len(t, result2.Interceptors, 1)
assert.Equal(t, "prompts-v", result2.Interceptors[0].Name)
// Filter for non-existent event — empty
result3, err := mcp.CallCustomMethod[*interceptors.ListParams, *interceptors.ListResult](
context.Background(), cs, interceptors.MethodList,
&interceptors.ListParams{Event: "nonexistent/method"},
)
require.NoError(t, err)
assert.Empty(t, result3.Interceptors)
}
// --- interceptor/invoke tests ---
func TestInvokeValidator(t *testing.T) {
t.Parallel()
cs := setupRPCServer(t, allowAllValidator("v1"))
payload, _ := json.Marshal(map[string]any{"name": "echo", "arguments": map[string]any{"text": "hello"}})
result, err := mcp.CallCustomMethod[*interceptors.InvokeParams, *interceptors.InvokeResult](
context.Background(), cs, interceptors.MethodInvoke,
&interceptors.InvokeParams{
Name: "v1",
Event: interceptors.EventToolsCall,
Phase: interceptors.PhaseRequest,
Payload: payload,
},
)
require.NoError(t, err)
assert.Equal(t, "v1", result.Interceptor)
assert.Equal(t, interceptors.TypeValidation, result.Type)
assert.Equal(t, interceptors.PhaseRequest, result.Phase)
require.NotNil(t, result.Validation)
assert.True(t, result.Validation.Valid)
assert.Nil(t, result.Mutation)
assert.Nil(t, result.Payload)
}
func TestInvokeValidatorRejects(t *testing.T) {
t.Parallel()
rejectAll := &interceptors.Validator{
Metadata: interceptors.Metadata{
Name: "reject-all",
Hooks: []interceptors.Hook{{
Events: []string{interceptors.EventToolsCall},
Phase: interceptors.PhaseRequest,
}},
Mode: interceptors.ModeActive,
},
Handler: func(_ context.Context, _ *interceptors.Invocation) (*interceptors.ValidationResult, error) {
return &interceptors.ValidationResult{
Valid: false,
Messages: []interceptors.ValidationMessage{
{Message: "rejected", Severity: interceptors.SeverityError},
},
}, nil
},
}
cs := setupRPCServer(t, rejectAll)
payload, _ := json.Marshal(map[string]any{"name": "echo"})
result, err := mcp.CallCustomMethod[*interceptors.InvokeParams, *interceptors.InvokeResult](
context.Background(), cs, interceptors.MethodInvoke,
&interceptors.InvokeParams{
Name: "reject-all",
Event: interceptors.EventToolsCall,
Phase: interceptors.PhaseRequest,
Payload: payload,
},
)
require.NoError(t, err)
assert.Equal(t, "reject-all", result.Interceptor)
assert.Equal(t, interceptors.TypeValidation, result.Type)
require.NotNil(t, result.Validation)
assert.False(t, result.Validation.Valid)
assert.Len(t, result.Validation.Messages, 1)
assert.Equal(t, "rejected", result.Validation.Messages[0].Message)
}
func TestInvokeMutator(t *testing.T) {
t.Parallel()
mutator := &interceptors.Mutator{
Metadata: interceptors.Metadata{
Name: "add-field",
Hooks: []interceptors.Hook{{
Events: []string{interceptors.EventToolsCall},
Phase: interceptors.PhaseRequest,
}},
Mode: interceptors.ModeActive,
},
Handler: func(_ context.Context, inv *interceptors.Invocation) (*interceptors.MutationResult, error) {
raw, ok := inv.Payload.(json.RawMessage)
if !ok {
return nil, fmt.Errorf("unexpected payload type %T", inv.Payload)
}
var m map[string]any
if err := json.Unmarshal(raw, &m); err != nil {
return nil, err
}
m["injected"] = true
data, err := json.Marshal(m)
if err != nil {
return nil, err
}
return &interceptors.MutationResult{Modified: true, Info: map[string]any{"added": "injected"}, Payload: json.RawMessage(data)}, nil
},
}
cs := setupRPCServer(t, mutator)
payload, _ := json.Marshal(map[string]any{"name": "echo"})
result, err := mcp.CallCustomMethod[*interceptors.InvokeParams, *interceptors.InvokeResult](
context.Background(), cs, interceptors.MethodInvoke,
&interceptors.InvokeParams{
Name: "add-field",
Event: interceptors.EventToolsCall,
Phase: interceptors.PhaseRequest,
Payload: payload,
},
)
require.NoError(t, err)
assert.Equal(t, "add-field", result.Interceptor)
assert.Equal(t, interceptors.TypeMutation, result.Type)
require.NotNil(t, result.Mutation)
assert.True(t, result.Mutation.Modified)
assert.Equal(t, "injected", result.Mutation.Info["added"])
// Verify mutated payload contains the injected field.
require.NotNil(t, result.Payload)
var mutated map[string]any
require.NoError(t, json.Unmarshal(result.Payload, &mutated))
assert.Equal(t, true, mutated["injected"])
assert.Equal(t, "echo", mutated["name"])
}
func TestInvokeUnknownName(t *testing.T) {
t.Parallel()
cs := setupRPCServer(t, allowAllValidator("v1"))
payload, _ := json.Marshal(map[string]any{})
_, err := mcp.CallCustomMethod[*interceptors.InvokeParams, *interceptors.InvokeResult](
context.Background(), cs, interceptors.MethodInvoke,
&interceptors.InvokeParams{
Name: "nonexistent",
Event: interceptors.EventToolsCall,
Phase: interceptors.PhaseRequest,
Payload: payload,
},
)
require.Error(t, err)
assert.Contains(t, err.Error(), "nonexistent")
}
func TestInvokeTimeout(t *testing.T) {
t.Parallel()
slowValidator := &interceptors.Validator{
Metadata: interceptors.Metadata{
Name: "slow-v",
Hooks: []interceptors.Hook{{
Events: []string{interceptors.EventToolsCall},
Phase: interceptors.PhaseRequest,
}},
Mode: interceptors.ModeActive,
},
Handler: func(ctx context.Context, _ *interceptors.Invocation) (*interceptors.ValidationResult, error) {
select {
case <-time.After(5 * time.Second):
return &interceptors.ValidationResult{Valid: true}, nil
case <-ctx.Done():
return nil, ctx.Err()
}
},
}
cs := setupRPCServer(t, slowValidator)
payload, _ := json.Marshal(map[string]any{})
_, err := mcp.CallCustomMethod[*interceptors.InvokeParams, *interceptors.InvokeResult](
context.Background(), cs, interceptors.MethodInvoke,
&interceptors.InvokeParams{
Name: "slow-v",
Event: interceptors.EventToolsCall,
Phase: interceptors.PhaseRequest,
Payload: payload,
TimeoutMs: 50,
},
)
require.Error(t, err)
}