feat(mcp): CallerResolver (AuthSession -> project.Caller bridge)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-08 10:34:47 +08:00
parent 319b3a8baa
commit 59e0138e7c
2 changed files with 129 additions and 0 deletions
+37
View File
@@ -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
}
+92
View File
@@ -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)
}