Files
Ilker ManapandClaude Opus 5.5 4453f469e1
CI / test (push) Failing after 5m4s
Company MCP servers are registered with ctl mcp, and tools granted with keys limits --tools
llmgw ctl mcp add|list|remove|test and /api/v1/mcp/servers keep the
servers; a token given with --secret-file is kept in the store, sealed,
and named by reference. keys limits --tools, and the console's limits
form, grant tools as patterns. Documented in install.md and clients.md.

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

108 lines
3.2 KiB
Go

package adminapi
import (
"context"
"strings"
"sync"
"testing"
"gitea.mynodes.xyz/ilker/llmgw/internal/mcpclient/mcptest"
"gitea.mynodes.xyz/ilker/llmgw/internal/secrets"
"gitea.mynodes.xyz/ilker/llmgw/internal/store"
)
type memMCP struct {
mu sync.Mutex
servers map[string]store.MCPServer
secrets map[string]string
}
func (m *memMCP) MCPServers(context.Context) ([]store.MCPServer, error) {
m.mu.Lock()
defer m.mu.Unlock()
var out []store.MCPServer
for _, s := range m.servers {
out = append(out, s)
}
return out, nil
}
func (m *memMCP) MCPServer(_ context.Context, name string) (store.MCPServer, error) {
m.mu.Lock()
defer m.mu.Unlock()
s, ok := m.servers[name]
if !ok {
return s, store.ErrNotFound
}
return s, nil
}
func (m *memMCP) PutMCPServer(_ context.Context, s store.MCPServer) error {
m.mu.Lock()
defer m.mu.Unlock()
m.servers[s.Name] = s
return nil
}
func (m *memMCP) DeleteMCPServer(_ context.Context, name string) error {
m.mu.Lock()
defer m.mu.Unlock()
if _, ok := m.servers[name]; !ok {
return store.ErrNotFound
}
delete(m.servers, name)
return nil
}
func (m *memMCP) SetClusterSecret(_ context.Context, name, value string) error {
m.mu.Lock()
defer m.mu.Unlock()
m.secrets[name] = value
return nil
}
func TestAnMCPServerIsRegisteredWithItsTokenKeptAsASecretAndTried(t *testing.T) {
srv := mcptest.New()
defer srv.Close()
a, _ := setup(t, testModels())
st := &memMCP{servers: map[string]store.MCPServer{}, secrets: map[string]string{}}
a.SetMCP(st)
secrets.SetStore(func(_ context.Context, name string) (string, error) {
st.mu.Lock()
defer st.mu.Unlock()
return st.secrets[name], nil
})
t.Cleanup(func() { secrets.SetStore(nil) })
var v MCPServerView
body := `{"url":"` + srv.URL + `","description":"tickets","headers":{"X-Org":"acme"},"secrets":{"Authorization":"Bearer t0ken"}}`
if code := call(t, a, "PUT", "/api/v1/mcp/servers/jira", body, &v); code != 200 {
t.Fatalf("add: %d", code)
}
// The token is in the store, named by reference; the row has none.
if st.secrets["mcp/jira/Authorization"] != "Bearer t0ken" || v.HeaderRefs["Authorization"] != "store:mcp/jira/Authorization" ||
strings.Contains(v.Headers["Authorization"], "t0ken") || !v.Enabled {
t.Errorf("view %+v, secrets %v", v, st.secrets)
}
var tv MCPTestView
if code := call(t, a, "POST", "/api/v1/mcp/servers/jira/test", "", &tv); code != 200 || !tv.OK || len(tv.Tools) != 3 || tv.Tools[1] != "jira.echo" {
t.Errorf("test: %d %+v", code, tv)
}
if code := call(t, a, "PUT", "/api/v1/mcp/servers/Bad.Name", body, nil); code != 400 {
t.Errorf("a name with a dot: %d", code)
}
if code := call(t, a, "PUT", "/api/v1/mcp/servers/x", `{"url":"ftp://x"}`, nil); code != 400 {
t.Errorf("not an http address: %d", code)
}
// Switched off, kept; then removed.
if code := call(t, a, "PUT", "/api/v1/mcp/servers/jira", `{"enabled":false}`, &v); code != 200 || v.Enabled || v.URL != srv.URL {
t.Errorf("off: %d %+v", code, v)
}
if code := call(t, a, "DELETE", "/api/v1/mcp/servers/jira", "", nil); code != 200 {
t.Errorf("remove: %d", code)
}
if code := call(t, a, "DELETE", "/api/v1/mcp/servers/jira", "", nil); code != 404 {
t.Errorf("remove again: %d", code)
}
}