From 59e0138e7c0601cae5117dd3665f1eacd225f4a6 Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Fri, 8 May 2026 10:34:47 +0800 Subject: [PATCH] feat(mcp): CallerResolver (AuthSession -> project.Caller bridge) Co-Authored-By: Claude Opus 4.6 --- internal/mcp/caller.go | 37 +++++++++++++++ internal/mcp/caller_test.go | 92 +++++++++++++++++++++++++++++++++++++ 2 files changed, 129 insertions(+) create mode 100644 internal/mcp/caller.go create mode 100644 internal/mcp/caller_test.go diff --git a/internal/mcp/caller.go b/internal/mcp/caller.go new file mode 100644 index 0000000..ee21d85 --- /dev/null +++ b/internal/mcp/caller.go @@ -0,0 +1,37 @@ +package mcp + +import ( + "context" + + "github.com/google/uuid" + + "github.com/yan1h/agent-coding-workflow/internal/infra/errs" + "github.com/yan1h/agent-coding-workflow/internal/project" + "github.com/yan1h/agent-coding-workflow/internal/user" +) + +// CallerResolver 把 MCP AuthSession 转成 project.Caller。 +type CallerResolver struct { + users user.Service +} + +func NewCallerResolver(users user.Service) *CallerResolver { + return &CallerResolver{users: users} +} + +func (r *CallerResolver) Resolve(ctx context.Context) (project.Caller, error) { + sess, ok := FromContext(ctx) + if !ok { + return project.Caller{}, errs.New(errs.CodeInternal, + "mcp: AuthSession missing from context (middleware order bug)") + } + uid, err := uuid.Parse(sess.UserID) + if err != nil { + return project.Caller{}, errs.Wrap(err, errs.CodeInternal, "parse user_id from auth session") + } + u, err := r.users.Get(ctx, uid) + if err != nil { + return project.Caller{}, err + } + return project.Caller{UserID: uid, IsAdmin: u.IsAdmin}, nil +} diff --git a/internal/mcp/caller_test.go b/internal/mcp/caller_test.go new file mode 100644 index 0000000..d500ea8 --- /dev/null +++ b/internal/mcp/caller_test.go @@ -0,0 +1,92 @@ +package mcp_test + +import ( + "context" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/yan1h/agent-coding-workflow/internal/infra/errs" + "github.com/yan1h/agent-coding-workflow/internal/mcp" + "github.com/yan1h/agent-coding-workflow/internal/user" +) + +type fakeUserSvc struct{ users map[uuid.UUID]*user.User } + +func (s *fakeUserSvc) Login(_ context.Context, _, _, _ string) (string, *user.User, error) { + return "", nil, nil +} +func (s *fakeUserSvc) Logout(_ context.Context, _ string) error { return nil } +func (s *fakeUserSvc) ResolveSession(_ context.Context, _ string) (uuid.UUID, bool, error) { + return uuid.Nil, false, nil +} +func (s *fakeUserSvc) Get(_ context.Context, id uuid.UUID) (*user.User, error) { + if u, ok := s.users[id]; ok { + return u, nil + } + return nil, errs.New(errs.CodeNotFound, "user") +} +func (s *fakeUserSvc) Bootstrap(_ context.Context, _, _ string) (*user.User, error) { + return nil, nil +} +func (s *fakeUserSvc) ListAdmins(_ context.Context) ([]*user.User, error) { return nil, nil } + +func ctxWithSession(s *mcp.AuthSession) context.Context { + return context.WithValue(context.Background(), mcp.AuthContextKey, s) +} + +func TestCallerResolver_Admin(t *testing.T) { + t.Parallel() + uid := uuid.New() + uSvc := &fakeUserSvc{users: map[uuid.UUID]*user.User{ + uid: {ID: uid, IsAdmin: true}, + }} + r := mcp.NewCallerResolver(uSvc) + + c, err := r.Resolve(ctxWithSession(&mcp.AuthSession{ + UserID: uid.String(), TokenID: uuid.NewString(), + })) + require.NoError(t, err) + assert.True(t, c.IsAdmin) + assert.Equal(t, uid, c.UserID) +} + +func TestCallerResolver_NonAdmin(t *testing.T) { + t.Parallel() + uid := uuid.New() + uSvc := &fakeUserSvc{users: map[uuid.UUID]*user.User{ + uid: {ID: uid, IsAdmin: false}, + }} + r := mcp.NewCallerResolver(uSvc) + + c, err := r.Resolve(ctxWithSession(&mcp.AuthSession{ + UserID: uid.String(), TokenID: uuid.NewString(), + })) + require.NoError(t, err) + assert.False(t, c.IsAdmin) +} + +func TestCallerResolver_NoSession(t *testing.T) { + t.Parallel() + r := mcp.NewCallerResolver(&fakeUserSvc{}) + + _, err := r.Resolve(context.Background()) + ae, ok := errs.As(err) + require.True(t, ok) + assert.Equal(t, errs.CodeInternal, ae.Code) +} + +func TestCallerResolver_UserDeleted(t *testing.T) { + t.Parallel() + uid := uuid.New() + r := mcp.NewCallerResolver(&fakeUserSvc{users: map[uuid.UUID]*user.User{}}) + + _, err := r.Resolve(ctxWithSession(&mcp.AuthSession{ + UserID: uid.String(), TokenID: uuid.NewString(), + })) + ae, ok := errs.As(err) + require.True(t, ok) + assert.Equal(t, errs.CodeNotFound, ae.Code) +}