diff --git a/pkg/agentic/platform_tools.go b/pkg/agentic/platform_tools.go index a615379..dc68751 100644 --- a/pkg/agentic/platform_tools.go +++ b/pkg/agentic/platform_tools.go @@ -120,6 +120,11 @@ func (s *PrepSubsystem) registerPlatformTools(svc *coremcp.Service) { Description: "Revoke a platform API key by key ID.", }, s.authRevokeTool) + coremcp.AddToolRecorded(svc, svc.Server(), "agentic", &mcp.Tool{ + Name: "agentic_auth_login", + Description: "Exchange a 6-digit pairing code (generated at app.lthn.ai/device) for an AgentApiKey. Bootstraps a fleet node without requiring an existing API key — RFC §9 Fleet Mode.", + }, s.authLoginTool) + coremcp.AddToolRecorded(svc, svc.Server(), "agentic", &mcp.Tool{ Name: "agentic_fleet_register", Description: "Register a fleet node with models, capabilities, and platform metadata.", @@ -256,6 +261,30 @@ func (s *PrepSubsystem) authRevokeTool(ctx context.Context, _ *mcp.CallToolReque return nil, output, nil } +// authLoginTool handles the MCP-side of the RFC §9 pairing-code bootstrap. +// Callers pass a 6-digit pairing code generated at app.lthn.ai/device and +// receive the provisioned AgentApiKey so the node can authenticate future +// platform calls. The code itself is the proof — no existing API key is +// required. +// +// Usage example: +// +// out, _ := clientSession.CallTool(ctx, &mcp.CallToolParams{ +// Name: "agentic_auth_login", +// Arguments: json.RawMessage(`{"code": "123456"}`), +// }) +func (s *PrepSubsystem) authLoginTool(ctx context.Context, _ *mcp.CallToolRequest, input AuthLoginInput) (*mcp.CallToolResult, AuthLoginOutput, error) { + result := s.handleAuthLogin(ctx, platformOptions(core.Option{Key: "code", Value: input.Code})) + if !result.OK { + return nil, AuthLoginOutput{}, resultErrorValue("agentic.auth.login", result) + } + output, ok := result.Value.(AuthLoginOutput) + if !ok { + return nil, AuthLoginOutput{}, core.E("agentic.auth.login", "invalid auth login output", nil) + } + return nil, output, nil +} + func (s *PrepSubsystem) fleetRegisterTool(ctx context.Context, _ *mcp.CallToolRequest, input FleetNode) (*mcp.CallToolResult, FleetNode, error) { options := platformOptions( core.Option{Key: "agent_id", Value: input.AgentID}, diff --git a/pkg/agentic/prep.go b/pkg/agentic/prep.go index 83bc576..d002cf3 100644 --- a/pkg/agentic/prep.go +++ b/pkg/agentic/prep.go @@ -595,7 +595,7 @@ func (s *PrepSubsystem) SetCore(c *core.Core) { // subsystem := agentic.NewPrep() // subsystem.RegisterTools(svc) func (s *PrepSubsystem) RegisterTools(svc *coremcp.Service) { - mcp.AddTool(svc.Server(), &mcp.Tool{ + coremcp.AddToolRecorded(svc, svc.Server(), "agentic", &mcp.Tool{ Name: "agentic_prep_workspace", Description: "Prepare an agent workspace: clone repo, create branch, build prompt with context.", }, s.prepWorkspace) @@ -617,7 +617,7 @@ func (s *PrepSubsystem) RegisterTools(svc *coremcp.Service) { s.registerWatchTool(svc) s.registerIssueTools(svc) s.registerPRTools(svc) - mcp.AddTool(svc.Server(), &mcp.Tool{ + coremcp.AddToolRecorded(svc, svc.Server(), "agentic", &mcp.Tool{ Name: "agentic_scan", Description: "Scan Forge repos for open issues with actionable labels (agentic, help-wanted, bug).", }, s.scan) @@ -642,14 +642,6 @@ func (s *PrepSubsystem) RegisterTools(svc *coremcp.Service) { s.registerContentTools(svc) s.registerLanguageTools(svc) s.registerSetupTool(svc) - - coremcp.AddToolRecorded(svc, svc.Server(), "agentic", &mcp.Tool{ - Name: "agentic_scan", - Description: "Scan Forge repos for open issues with actionable labels (agentic, help-wanted, bug).", - }, s.scan) - - s.registerPlanTools(svc) - s.registerWatchTool(svc) } // subsystem := agentic.NewPrep() diff --git a/pkg/agentic/prep_test.go b/pkg/agentic/prep_test.go index 2678614..4e14caa 100644 --- a/pkg/agentic/prep_test.go +++ b/pkg/agentic/prep_test.go @@ -739,6 +739,12 @@ func TestPrep_RegisterTools_Good_RegistersCompletionTool(t *testing.T) { assert.Contains(t, toolNames, "agent_inbox") assert.Contains(t, toolNames, "agentic_message_conversation") assert.Contains(t, toolNames, "agent_conversation") + // RFC §9 pairing-code bootstrap exposes the login flow as an MCP tool so + // IDE/CLI callers can exchange a 6-digit code for an AgentApiKey without + // shelling out. + assert.Contains(t, toolNames, "agentic_auth_login") + assert.Contains(t, toolNames, "agentic_auth_provision") + assert.Contains(t, toolNames, "agentic_auth_revoke") } func TestPrep_OnStartup_Good_RegistersGenerateCommand(t *testing.T) {