<!-- Provide a brief summary of your changes -->
Update the Go interceptor SDK to use
`github.com/modelcontextprotocol/go-sdk` v1.7.0 custom method APIs and
remove the vendored Go SDK copy. This shouldn't update the behaviours,
but addding following functions.
Changed public/user-facing interfaces:
extension.Extension.Install now returns an error:
```
// before
ext.Install(mcpServer)
// after
if err := ext.Install(mcpServer); err != nil {
return err
}
```
New helper added:
```
extension.RegisterSendingMethods(client *mcp.Client) error
```
## Motivation and Context
<!-- Why is this change needed? What problem does it solve? -->
Pick up MCP Go SDK 1.7.0. This provides upstream custom method support,
so the local vendored SDK copy is no longer needed.
## How Has This Been Tested?
<!-- Have you tested this in a real application? Which scenarios were
tested? -->
- `go test -v ./...`
## Breaking Changes
<!-- Will users need to update their code or configurations? -->
```go
if err := ext.Install(mcpServer); err != nil {
return err
}
```
The Go SDK module now requires Go 1.25 via
github.com/modelcontextprotocol/go-sdk v1.7.0.
## 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)
- [x] 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
- [x] 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
-->
LocalChain now registers interceptor custom sending methods internally.
Manual remote chain setup should call
`extension.RegisterSendingMethods(client)` before connecting the MCP
client session used with chain.AddMCPServer.
Signed-off-by: Ukjae Jeong <jeongukjae@gmail.com>
274 lines
8.1 KiB
Go
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.ModeEnforce,
|
|
},
|
|
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.ModeEnforce,
|
|
},
|
|
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.ModeEnforce,
|
|
},
|
|
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.ModeEnforce,
|
|
},
|
|
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)
|
|
}
|