email-mcp/internal/cli/integration_test.go
thibaud-leclere 92fc30cb2d fix: tolerate unknown fields in MCP protocol messages and defer credential loading
Claude Code sends extra fields (e.g. "title") in initialize params that
caused the server to reject the request due to DisallowUnknownFields.
Use lenient JSON decoding for protocol messages while keeping strict
validation for tool arguments. Also defer KWallet credential loading
from server startup to tool invocation time, and negotiate protocol
versions per MCP spec instead of rejecting unknown ones.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 15:46:53 +02:00

94 lines
3 KiB
Go

package cli
import (
"bytes"
"context"
"testing"
"email-mcp/internal/imapclient"
"email-mcp/internal/mcpserver"
"email-mcp/internal/secretstore"
"email-mcp/internal/secretstore/kwallet"
)
type entrypointPromptStub struct {
credential secretstore.Credential
}
func (p *entrypointPromptStub) PromptSetup(context.Context) (secretstore.Credential, error) {
return p.credential, nil
}
type entrypointStoreStub struct {
saveErr error
loadErr error
}
func (s *entrypointStoreStub) Save(context.Context, string, secretstore.Credential) error {
return s.saveErr
}
func (s *entrypointStoreStub) Load(context.Context, string) (secretstore.Credential, error) {
return secretstore.Credential{}, s.loadErr
}
type entrypointMailServiceStub struct{}
func (entrypointMailServiceStub) ListMailboxes(context.Context, secretstore.Credential) ([]imapclient.Mailbox, error) {
return nil, nil
}
func (entrypointMailServiceStub) ListMessages(context.Context, secretstore.Credential, string, int) ([]imapclient.MessageSummary, error) {
return nil, nil
}
func (entrypointMailServiceStub) GetMessage(context.Context, secretstore.Credential, string, uint32) (imapclient.Message, error) {
return imapclient.Message{}, nil
}
func TestExecuteSetupWritesWalletGuidanceAndReturnsExitCodeOne(t *testing.T) {
app := NewAppWithDependencies(
&entrypointPromptStub{
credential: secretstore.Credential{
Host: "imap.example.com",
Username: "alice",
Password: "secret",
},
},
&entrypointStoreStub{saveErr: kwallet.ErrKWalletUnavailable},
nil,
nil,
)
stderr := &bytes.Buffer{}
if code := Execute(app, []string{"setup"}, stderr); code != 1 {
t.Fatalf("expected exit code 1, got %d", code)
}
if got := stderr.String(); got != "kwallet is not available; make sure KDE Wallet is installed and your session D-Bus is running\n" {
t.Fatalf("unexpected stderr: %q", got)
}
}
func TestExecuteMCPReturnsMissingCredentialErrorOnToolCall(t *testing.T) {
store := &entrypointStoreStub{loadErr: kwallet.ErrCredentialNotFound}
mail := entrypointMailServiceStub{}
input := bytes.NewBufferString(
"{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{\"protocolVersion\":\"2024-11-05\",\"capabilities\":{},\"clientInfo\":{\"name\":\"test\",\"version\":\"1.0.0\"}}}\n" +
"{\"jsonrpc\":\"2.0\",\"method\":\"notifications/initialized\"}\n" +
"{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/call\",\"params\":{\"name\":\"list_mailboxes\"}}\n",
)
output := &bytes.Buffer{}
runner := mcpserver.NewRunner(mcpserver.New(store, mail), input, output, &bytes.Buffer{})
app := NewAppWithDependencies(nil, store, runner, nil)
stderr := &bytes.Buffer{}
if code := Execute(app, []string{"mcp"}, stderr); code != 0 {
t.Fatalf("expected exit code 0, got %d; stderr: %s", code, stderr.String())
}
// Verify the credential error appears in the tool call response
got := output.String()
if !bytes.Contains([]byte(got), []byte("credentials not configured")) {
t.Fatalf("expected credential error in output, got %q", got)
}
}