Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 102 additions & 20 deletions cmd/mcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,32 +10,59 @@ import (
"github.com/spf13/cobra"

"github.com/authorizerdev/authorizer/internal/audit"
"github.com/authorizerdev/authorizer/internal/authorization"
"github.com/authorizerdev/authorizer/internal/constants"
"github.com/authorizerdev/authorizer/internal/email"
"github.com/authorizerdev/authorizer/internal/events"
"github.com/authorizerdev/authorizer/internal/grpcsrv"
"github.com/authorizerdev/authorizer/internal/mcp"
"github.com/authorizerdev/authorizer/internal/memory_store"
"github.com/authorizerdev/authorizer/internal/service"
"github.com/authorizerdev/authorizer/internal/sms"
"github.com/authorizerdev/authorizer/internal/storage"
"github.com/authorizerdev/authorizer/internal/token"
)

// mcpArgs are the MCP-subcommand-only flags. The root command's flags
// (--database-type, --client-id, --jwt-secret, ...) are inherited by the
// subcommand automatically since they live on RootCmd.
var mcpArgs struct {
// bearer is propagated as `Authorization: Bearer <bearer>` on every
// outgoing gRPC call. Without it the MCP server runs anonymously —
// fine for the `meta` tool (public) but identity-bearing tools
// (`profile`, `permissions`) won't have a caller to attribute to.
bearer string
}

// mcpCmd serves Authorizer's MCP surface over stdio. Designed to be wired
// into Claude Code or any other MCP host via:
//
// claude mcp add authorizer -- /path/to/authorizer mcp --client-id=... \
// --database-type=sqlite --database-url=auth.db
// --database-type=sqlite --database-url=auth.db --mcp-bearer=$TOKEN
//
// Which tools are exposed is declared at the proto layer via the
// `(authorizer.common.v1.mcp_tool).exposed` option; the MCP server discovers
// them at startup. Today: GetMeta. As more public ops migrate into
// internal/service and get the mcp_tool annotation, they appear automatically.
// them at startup.
//
// Transport: STDIO ONLY. The MCP server has no auth/rate-limit interceptors
// of its own — the security model relies on the OS-level trust boundary of
// the subprocess. See internal/mcp/server.go's Server type comment.
var mcpCmd = &cobra.Command{
Use: "mcp",
Short: "Serve Authorizer's MCP tool surface over stdio",
Long: "Exposes a subset of Authorizer's gRPC methods (those marked " +
"(authorizer.common.v1.mcp_tool).exposed=true in proto) as MCP " +
"tools, suitable for use with Claude Code or any MCP-compatible host.",
"tools, suitable for use with Claude Code or any MCP-compatible " +
"host. Stdio is the only supported transport.",
Run: runMCP,
}

func init() {
mcpCmd.Flags().StringVar(&mcpArgs.bearer, "mcp-bearer", "",
"Bearer token to attach to every outgoing gRPC call (carries the "+
"user identity for tools like Profile / Permissions / Session). "+
"When unset the MCP server runs anonymously; public tools (Meta) "+
"still work but identity-bearing tools will fail authn.")
RootCmd.AddCommand(mcpCmd)
}

Expand All @@ -44,22 +71,73 @@ func runMCP(_ *cobra.Command, _ []string) {
// JSON-RPC framing on stdout.
log := zerolog.New(os.Stderr).With().Timestamp().Logger()

// For the GetMeta-only vertical slice we don't need storage / token /
// memory store / events / email / sms. As more MCP-exposed tools come
// online (Phase 4+ migrations of ListMyPermissions, GetCurrentSession,
// GetUser(me)) wire them in following the same pattern as runRoot.
// Wire all subsystems an MCP-exposed tool might need. As more ops
// migrate into internal/service, this list stays the same — the
// service-provider dependencies don't change per op, only the methods
// on the provider do.
storageProvider, err := storage.New(&rootArgs.config, &storage.Dependencies{Log: &log})
if err != nil {
log.Fatal().Err(err).Msg("failed to create storage provider")
}
memoryStoreProvider, err := memory_store.New(&rootArgs.config, &memory_store.Dependencies{
Log: &log,
StorageProvider: storageProvider,
})
if err != nil {
log.Fatal().Err(err).Msg("failed to create memory store provider")
}
tokenProvider, err := token.New(&rootArgs.config, &token.Dependencies{
Log: &log,
MemoryStoreProvider: memoryStoreProvider,
})
if err != nil {
log.Fatal().Err(err).Msg("failed to create token provider")
}
emailProvider, err := email.New(&rootArgs.config, &email.Dependencies{
Log: &log,
StorageProvider: storageProvider,
})
if err != nil {
log.Fatal().Err(err).Msg("failed to create email provider")
}
smsProvider, err := sms.New(&rootArgs.config, &sms.Dependencies{Log: &log})
if err != nil {
log.Fatal().Err(err).Msg("failed to create sms provider")
}
auditProvider := audit.New(&audit.Dependencies{
Log: &log,
StorageProvider: storageProvider,
})
eventsProvider, err := events.New(&rootArgs.config, &events.Dependencies{
Log: &log,
StorageProvider: storageProvider,
})
if err != nil {
log.Fatal().Err(err).Msg("failed to create events provider")
}

authorizationProvider, err := authorization.New(
&authorization.Config{CacheTTL: 0},
&authorization.Dependencies{
Log: &log,
StorageProvider: storageProvider,
MemoryStoreProvider: memoryStoreProvider,
},
)
if err != nil {
log.Fatal().Err(err).Msg("failed to create authorization provider")
}

svc, err := service.New(&rootArgs.config, &service.Dependencies{
Log: &log,
// nil-safe: methods that need these subsystems are not yet exposed
// as MCP tools. Each panics-on-nil call moved here would be caught
// by integration tests before reaching prod.
AuditProvider: audit.New(&audit.Dependencies{Log: &log}),
EmailProvider: nil,
EventsProvider: nil,
MemoryStoreProvider: nil,
SMSProvider: nil,
StorageProvider: nil,
TokenProvider: nil,
Log: &log,
AuditProvider: auditProvider,
AuthorizationProvider: authorizationProvider,
EmailProvider: emailProvider,
EventsProvider: eventsProvider,
MemoryStoreProvider: memoryStoreProvider,
SMSProvider: smsProvider,
StorageProvider: storageProvider,
TokenProvider: tokenProvider,
})
if err != nil {
log.Fatal().Err(err).Msg("failed to create service provider")
Expand All @@ -74,7 +152,11 @@ func runMCP(_ *cobra.Command, _ []string) {
log.Fatal().Err(err).Msg("failed to create grpc server")
}

mcpSrv, err := mcp.New(&log, grpcSrv.GRPCServer(), "authorizer", constants.VERSION)
mcpSrv, err := mcp.New(&log, grpcSrv.GRPCServer(), mcp.Options{
Name: "authorizer",
Version: constants.VERSION,
Bearer: mcpArgs.bearer,
})
if err != nil {
log.Fatal().Err(err).Msg("failed to create mcp server")
}
Expand Down
22 changes: 11 additions & 11 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -553,18 +553,18 @@ func runRoot(c *cobra.Command, args []string) {
StorageProvider: storageProvider,
})

// Transport-agnostic service layer that hosts public-API operations
// (currently SignUp; more migrate over in subsequent phases). GraphQL,
// gRPC, and REST surfaces all delegate to this.
// Transport-agnostic service layer that hosts public-API operations.
// GraphQL, gRPC, and REST surfaces all delegate to this.
serviceProvider, err := service.New(&rootArgs.config, &service.Dependencies{
Log: &log,
AuditProvider: auditProvider,
EmailProvider: emailProvider,
EventsProvider: eventsProvider,
MemoryStoreProvider: memoryStoreProvider,
SMSProvider: smsProvider,
StorageProvider: storageProvider,
TokenProvider: tokenProvider,
Log: &log,
AuditProvider: auditProvider,
AuthorizationProvider: authorizationProvider,
EmailProvider: emailProvider,
EventsProvider: eventsProvider,
MemoryStoreProvider: memoryStoreProvider,
SMSProvider: smsProvider,
StorageProvider: storageProvider,
TokenProvider: tokenProvider,
})
if err != nil {
log.Fatal().Err(err).Msg("failed to create service provider")
Expand Down
95 changes: 47 additions & 48 deletions gen/go/authorizer/v1/authorizer.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions gen/go/authorizer/v1/authorizer_grpc.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading