Files
llmgw/internal/mcpclient/mcpclient_test.go
T
Ilker ManapandClaude Opus 5.5 765b6f5603 An MCP client for the company's tool servers
internal/mcpclient speaks streamable HTTP: initializes once and keeps
the session the server gives, beginning it again once if the server
forgot it; lists tools page by page; calls one with headers of its own,
taking the answer as JSON or as events with progress before it; and
bounds what it reads. mcptest is a server for tests.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 10:43:35 +02:00

88 lines
2.7 KiB
Go

package mcpclient
import (
"context"
"encoding/json"
"errors"
"net/http"
"strings"
"testing"
"gitea.mynodes.xyz/ilker/llmgw/internal/mcpclient/mcptest"
)
func TestToolsAreListedPageByPageAndCalledInOneSession(t *testing.T) {
srv := mcptest.New()
defer srv.Close()
srv.PageSize = 2
c := &Client{URL: srv.URL, Header: func(context.Context) (http.Header, error) {
return http.Header{"Authorization": {"Bearer service"}}, nil
}}
tools, err := c.Tools(t.Context())
if err != nil || len(tools) != 3 || tools[2].Name != "big" {
t.Fatalf("tools %+v %v", tools, err)
}
raw, err := c.Call(t.Context(), "echo", json.RawMessage(`{"text":"hello"}`), http.Header{"X-Llmgw-User": {"ada"}}, nil)
if err != nil || !strings.Contains(string(raw), "hello") {
t.Fatalf("call %s %v", raw, err)
}
if srv.Sessions() != 1 {
t.Errorf("initialized %d times", srv.Sessions())
}
h := srv.Headers[0]
if h.Get("Authorization") != "Bearer service" || h.Get("X-Llmgw-User") != "ada" || h.Get("MCP-Protocol-Version") != "2025-06-18" {
t.Errorf("headers %v", h)
}
}
func TestAStreamedAnswerIsReadAndItsProgressHeard(t *testing.T) {
srv := mcptest.New()
defer srv.Close()
srv.Stream = true
c := &Client{URL: srv.URL}
var heard []string
raw, err := c.Call(t.Context(), "echo", json.RawMessage(`{"text":"streamed"}`), nil, func(p json.RawMessage) {
heard = append(heard, string(p))
})
if err != nil || !strings.Contains(string(raw), "streamed") {
t.Fatalf("call %s %v", raw, err)
}
if len(heard) != 1 || !strings.Contains(heard[0], "halfway") {
t.Errorf("progress %v", heard)
}
}
func TestASessionTheServerForgotIsBegunAgainOnce(t *testing.T) {
srv := mcptest.New()
defer srv.Close()
c := &Client{URL: srv.URL}
if _, err := c.Tools(t.Context()); err != nil {
t.Fatal(err)
}
srv.Forget()
if _, err := c.Call(t.Context(), "echo", json.RawMessage(`{"text":"again"}`), nil, nil); err != nil {
t.Fatalf("after the server restarted: %v", err)
}
if srv.Sessions() != 2 {
t.Errorf("sessions %d", srv.Sessions())
}
}
func TestTheServersErrorsAndAnAnswerTooLargeAreSaid(t *testing.T) {
srv := mcptest.New()
defer srv.Close()
c := &Client{URL: srv.URL, MaxBytes: 64 << 10}
var rpcErr *Error
if _, err := c.Call(t.Context(), "nosuch", nil, nil, nil); !errors.As(err, &rpcErr) || rpcErr.Code != -32602 {
t.Errorf("unknown tool: %v", err)
}
if _, err := c.Call(t.Context(), "big", nil, nil, nil); err == nil || !strings.Contains(err.Error(), "larger than") {
t.Errorf("a megabyte through a 64 KiB limit: %v", err)
}
// A tool's own failure is its result, not an error of the protocol.
raw, err := c.Call(t.Context(), "fail", nil, nil, nil)
if err != nil || !strings.Contains(string(raw), `"isError":true`) {
t.Errorf("fail: %s %v", raw, err)
}
}