mirror of
https://github.com/humanlayer/humanlayer.git
synced 2025-08-20 19:01:22 +03:00
Phase 1: Restore MCP stdio server in hlyr
- Create mcp.ts with claude_approvals command for local daemon integration - Register MCP command in index.ts - Uses daemon RPC instead of HumanLayer API for approval management
This commit is contained in:
@@ -29,6 +29,7 @@ type CreateApprovalRequest struct {
|
||||
RunID string `json:"run_id"`
|
||||
ToolName string `json:"tool_name"`
|
||||
ToolInput json.RawMessage `json:"tool_input"`
|
||||
ToolUseID string `json:"tool_use_id,omitempty"`
|
||||
}
|
||||
|
||||
// CreateApprovalResponse is the response for creating a local approval
|
||||
@@ -54,11 +55,23 @@ func (h *ApprovalHandlers) HandleCreateApproval(ctx context.Context, params json
|
||||
return nil, fmt.Errorf("tool_input is required")
|
||||
}
|
||||
|
||||
// Create the approval
|
||||
approvalID, err := h.approvals.CreateApproval(ctx, req.RunID, req.ToolName, req.ToolInput)
|
||||
// Create approval with or without tool use ID
|
||||
var approvalID string
|
||||
if req.ToolUseID != "" {
|
||||
// Use the new method that accepts tool use ID
|
||||
approval, err := h.approvals.CreateApprovalWithToolUseID(ctx, req.RunID, req.ToolName, req.ToolInput, req.ToolUseID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create approval with tool use ID: %w", err)
|
||||
}
|
||||
approvalID = approval.ID
|
||||
} else {
|
||||
// Fall back to legacy method for backward compatibility
|
||||
var err error
|
||||
approvalID, err = h.approvals.CreateApproval(ctx, req.RunID, req.ToolName, req.ToolInput)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create approval: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return &CreateApprovalResponse{
|
||||
ApprovalID: approvalID,
|
||||
|
||||
@@ -344,18 +344,73 @@ func TestContinueSessionInheritance(t *testing.T) {
|
||||
t.Fatalf("Failed to get child MCP servers: %v", err)
|
||||
}
|
||||
|
||||
// Should have inherited the MCP servers
|
||||
if len(childMCPServers) != len(mcpServers) {
|
||||
t.Errorf("MCP servers not inherited: got %d, want %d", len(childMCPServers), len(mcpServers))
|
||||
// Should have inherited the MCP servers plus injected codelayer
|
||||
expectedCount := len(mcpServers) + 1 // +1 for injected codelayer
|
||||
if len(childMCPServers) != expectedCount {
|
||||
t.Errorf("MCP servers count mismatch: got %d, want %d", len(childMCPServers), expectedCount)
|
||||
}
|
||||
|
||||
// Verify server details (accounting for HUMANLAYER_RUN_ID being added)
|
||||
// Find and verify the injected codelayer server
|
||||
var foundCodelayer bool
|
||||
var codelayerIdx int
|
||||
for i, server := range childMCPServers {
|
||||
if server.Name != mcpServers[i].Name {
|
||||
t.Errorf("MCP server %d name mismatch: got %s, want %s", i, server.Name, mcpServers[i].Name)
|
||||
if server.Name == "codelayer" {
|
||||
foundCodelayer = true
|
||||
codelayerIdx = i
|
||||
|
||||
// Verify codelayer configuration
|
||||
if server.Command != "hlyr" {
|
||||
t.Errorf("Codelayer command mismatch: got %s, want hlyr", server.Command)
|
||||
}
|
||||
if server.Command != mcpServers[i].Command {
|
||||
t.Errorf("MCP server %d command mismatch: got %s, want %s", i, server.Command, mcpServers[i].Command)
|
||||
|
||||
var args []string
|
||||
if err := json.Unmarshal([]byte(server.ArgsJSON), &args); err != nil {
|
||||
t.Fatalf("Failed to unmarshal codelayer args: %v", err)
|
||||
}
|
||||
expectedArgs := []string{"mcp", "claude_approvals"}
|
||||
if len(args) != len(expectedArgs) {
|
||||
t.Errorf("Codelayer args length mismatch: got %d, want %d", len(args), len(expectedArgs))
|
||||
} else {
|
||||
for j, arg := range args {
|
||||
if arg != expectedArgs[j] {
|
||||
t.Errorf("Codelayer arg[%d] mismatch: got %s, want %s", j, arg, expectedArgs[j])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var env map[string]string
|
||||
if err := json.Unmarshal([]byte(server.EnvJSON), &env); err != nil {
|
||||
t.Fatalf("Failed to unmarshal codelayer env: %v", err)
|
||||
}
|
||||
if env["HUMANLAYER_SESSION_ID"] != childSession.ID {
|
||||
t.Errorf("Codelayer env HUMANLAYER_SESSION_ID mismatch: got %s, want %s", env["HUMANLAYER_SESSION_ID"], childSession.ID)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !foundCodelayer {
|
||||
t.Error("Injected codelayer MCP server not found")
|
||||
}
|
||||
|
||||
// Verify inherited servers (excluding codelayer)
|
||||
parentIdx := 0
|
||||
for i, server := range childMCPServers {
|
||||
// Skip the codelayer server
|
||||
if i == codelayerIdx {
|
||||
continue
|
||||
}
|
||||
|
||||
if parentIdx >= len(mcpServers) {
|
||||
t.Errorf("Extra unexpected MCP server found: %s", server.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
if server.Name != mcpServers[parentIdx].Name {
|
||||
t.Errorf("MCP server %d name mismatch: got %s, want %s", parentIdx, server.Name, mcpServers[parentIdx].Name)
|
||||
}
|
||||
if server.Command != mcpServers[parentIdx].Command {
|
||||
t.Errorf("MCP server %d command mismatch: got %s, want %s", parentIdx, server.Command, mcpServers[parentIdx].Command)
|
||||
}
|
||||
|
||||
// Compare args (deserialize to compare content)
|
||||
@@ -363,15 +418,15 @@ func TestContinueSessionInheritance(t *testing.T) {
|
||||
if err := json.Unmarshal([]byte(server.ArgsJSON), &childArgs); err != nil {
|
||||
t.Fatalf("Failed to unmarshal child args: %v", err)
|
||||
}
|
||||
if err := json.Unmarshal([]byte(mcpServers[i].ArgsJSON), &parentArgs); err != nil {
|
||||
if err := json.Unmarshal([]byte(mcpServers[parentIdx].ArgsJSON), &parentArgs); err != nil {
|
||||
t.Fatalf("Failed to unmarshal parent args: %v", err)
|
||||
}
|
||||
if len(childArgs) != len(parentArgs) {
|
||||
t.Errorf("MCP server %d args length mismatch", i)
|
||||
t.Errorf("MCP server %d args length mismatch", parentIdx)
|
||||
} else {
|
||||
for j, arg := range childArgs {
|
||||
if arg != parentArgs[j] {
|
||||
t.Errorf("MCP server %d arg[%d] mismatch: got %s, want %s", i, j, arg, parentArgs[j])
|
||||
t.Errorf("MCP server %d arg[%d] mismatch: got %s, want %s", parentIdx, j, arg, parentArgs[j])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -381,21 +436,23 @@ func TestContinueSessionInheritance(t *testing.T) {
|
||||
if err := json.Unmarshal([]byte(server.EnvJSON), &childEnv); err != nil {
|
||||
t.Fatalf("Failed to unmarshal child env: %v", err)
|
||||
}
|
||||
if err := json.Unmarshal([]byte(mcpServers[i].EnvJSON), &parentEnv); err != nil {
|
||||
if err := json.Unmarshal([]byte(mcpServers[parentIdx].EnvJSON), &parentEnv); err != nil {
|
||||
t.Fatalf("Failed to unmarshal parent env: %v", err)
|
||||
}
|
||||
|
||||
// Child should have all parent env vars plus HUMANLAYER_RUN_ID
|
||||
for key, val := range parentEnv {
|
||||
if childEnv[key] != val {
|
||||
t.Errorf("MCP server %d env[%s] mismatch: got %s, want %s", i, key, childEnv[key], val)
|
||||
t.Errorf("MCP server %d env[%s] mismatch: got %s, want %s", parentIdx, key, childEnv[key], val)
|
||||
}
|
||||
}
|
||||
|
||||
// Should have HUMANLAYER_RUN_ID added
|
||||
if _, ok := childEnv["HUMANLAYER_RUN_ID"]; !ok {
|
||||
t.Errorf("MCP server %d missing HUMANLAYER_RUN_ID in env", i)
|
||||
t.Errorf("MCP server %d missing HUMANLAYER_RUN_ID in env", parentIdx)
|
||||
}
|
||||
|
||||
parentIdx++
|
||||
}
|
||||
})
|
||||
|
||||
@@ -576,17 +633,26 @@ func TestContinueSessionInheritance(t *testing.T) {
|
||||
t.Fatalf("Failed to get child MCP servers: %v", err)
|
||||
}
|
||||
|
||||
// Should have the override server, not the original
|
||||
if len(childMCPServers) != 1 {
|
||||
t.Fatalf("Expected 1 MCP server, got %d", len(childMCPServers))
|
||||
// Should have the override server plus injected codelayer
|
||||
if len(childMCPServers) != 2 {
|
||||
t.Fatalf("Expected 2 MCP servers (override + codelayer), got %d", len(childMCPServers))
|
||||
}
|
||||
|
||||
server := childMCPServers[0]
|
||||
if server.Name != "override-server" {
|
||||
t.Errorf("MCP server name not overridden: got %s", server.Name)
|
||||
// Find the override server (not codelayer)
|
||||
var overrideServer *store.MCPServer
|
||||
for _, s := range childMCPServers {
|
||||
if s.Name == "override-server" {
|
||||
overrideServer = &s
|
||||
break
|
||||
}
|
||||
if server.Command != "override-cmd" {
|
||||
t.Errorf("MCP server command not overridden: got %s", server.Command)
|
||||
}
|
||||
|
||||
if overrideServer == nil {
|
||||
t.Fatal("Override server not found")
|
||||
}
|
||||
|
||||
if overrideServer.Command != "override-cmd" {
|
||||
t.Errorf("MCP server command not overridden: got %s", overrideServer.Command)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -853,17 +919,25 @@ func TestContinueSessionInheritance(t *testing.T) {
|
||||
t.Fatalf("Failed to get child MCP servers: %v", err)
|
||||
}
|
||||
|
||||
// Should have inherited the MCP server
|
||||
if len(childMCPServers) != 1 {
|
||||
t.Fatalf("Expected 1 MCP server, got %d", len(childMCPServers))
|
||||
// Should have inherited the MCP server plus injected codelayer
|
||||
if len(childMCPServers) != 2 {
|
||||
t.Fatalf("Expected 2 MCP servers (http + codelayer), got %d", len(childMCPServers))
|
||||
}
|
||||
|
||||
childMCPServer := childMCPServers[0]
|
||||
// Find the http test server (not codelayer)
|
||||
var childMCPServer *store.MCPServer
|
||||
for _, s := range childMCPServers {
|
||||
if s.Name == "http-test-server" {
|
||||
childMCPServer = &s
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if childMCPServer == nil {
|
||||
t.Fatal("HTTP test server not found")
|
||||
}
|
||||
|
||||
// Verify basic inheritance
|
||||
if childMCPServer.Name != "http-test-server" {
|
||||
t.Errorf("MCP server name not inherited: got %s, want http-test-server", childMCPServer.Name)
|
||||
}
|
||||
if childMCPServer.Command != "http" {
|
||||
t.Errorf("MCP server type not inherited: got %s, want http", childMCPServer.Command)
|
||||
}
|
||||
|
||||
@@ -68,6 +68,26 @@ func (m *Manager) LaunchSession(ctx context.Context, config LaunchSessionConfig)
|
||||
// Extract the Claude config (without daemon-level settings)
|
||||
claudeConfig := config.SessionConfig
|
||||
|
||||
// Inject daemon's CodeLayer MCP server configuration
|
||||
if claudeConfig.MCPConfig == nil {
|
||||
claudeConfig.MCPConfig = &claudecode.MCPConfig{
|
||||
MCPServers: make(map[string]claudecode.MCPServer),
|
||||
}
|
||||
}
|
||||
|
||||
// Always inject codelayer MCP server (overwrite if exists)
|
||||
claudeConfig.MCPConfig.MCPServers["codelayer"] = claudecode.MCPServer{
|
||||
Command: "hlyr",
|
||||
Args: []string{"mcp", "claude_approvals"},
|
||||
Env: map[string]string{
|
||||
"HUMANLAYER_SESSION_ID": sessionID,
|
||||
"HUMANLAYER_DAEMON_SOCKET": m.socketPath,
|
||||
},
|
||||
}
|
||||
slog.Debug("injected codelayer MCP server",
|
||||
"session_id", sessionID,
|
||||
"socket_path", m.socketPath)
|
||||
|
||||
// Add HUMANLAYER_RUN_ID and HUMANLAYER_DAEMON_SOCKET to MCP server environment
|
||||
// For HTTP servers, inject session ID header
|
||||
if claudeConfig.MCPConfig != nil {
|
||||
@@ -1300,8 +1320,33 @@ func (m *Manager) ContinueSession(ctx context.Context, req ContinueSessionConfig
|
||||
// Add run_id and daemon socket to MCP server environments
|
||||
// For HTTP servers, inject session ID header
|
||||
|
||||
// Ensure MCP config exists for injection
|
||||
if config.MCPConfig == nil {
|
||||
config.MCPConfig = &claudecode.MCPConfig{
|
||||
MCPServers: make(map[string]claudecode.MCPServer),
|
||||
}
|
||||
}
|
||||
|
||||
// Always update codelayer MCP server with child session ID
|
||||
config.MCPConfig.MCPServers["codelayer"] = claudecode.MCPServer{
|
||||
Command: "hlyr",
|
||||
Args: []string{"mcp", "claude_approvals"},
|
||||
Env: map[string]string{
|
||||
"HUMANLAYER_SESSION_ID": sessionID, // Use child session ID
|
||||
"HUMANLAYER_DAEMON_SOCKET": m.socketPath,
|
||||
},
|
||||
}
|
||||
slog.Debug("updated codelayer MCP server for child session",
|
||||
"session_id", sessionID,
|
||||
"parent_session_id", req.ParentSessionID,
|
||||
"socket_path", m.socketPath)
|
||||
|
||||
if config.MCPConfig != nil {
|
||||
for name, server := range config.MCPConfig.MCPServers {
|
||||
// Skip codelayer as we already configured it above
|
||||
if name == "codelayer" {
|
||||
continue
|
||||
}
|
||||
// Check if this is an HTTP MCP server
|
||||
if server.Type == "http" {
|
||||
// For HTTP servers, always set session ID header to child session ID
|
||||
|
||||
@@ -337,11 +337,25 @@ func TestLaunchSession_SetsMCPEnvironment(t *testing.T) {
|
||||
}
|
||||
|
||||
// Verify MCP servers have the correct environment variables
|
||||
if len(capturedMCPServers) != 1 {
|
||||
t.Fatalf("Expected 1 MCP server, got %d", len(capturedMCPServers))
|
||||
// Should have the test server plus injected codelayer
|
||||
if len(capturedMCPServers) != 2 {
|
||||
t.Fatalf("Expected 2 MCP servers (test + codelayer), got %d", len(capturedMCPServers))
|
||||
}
|
||||
|
||||
server := capturedMCPServers[0]
|
||||
// Find the test server (not codelayer)
|
||||
var server store.MCPServer
|
||||
var found bool
|
||||
for _, s := range capturedMCPServers {
|
||||
if s.Name == "test-server" {
|
||||
server = s
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
t.Fatal("Test server not found in MCP servers")
|
||||
}
|
||||
|
||||
// Parse the environment JSON
|
||||
var env map[string]string
|
||||
|
||||
@@ -45,22 +45,6 @@ export const launchCommand = async (query: string, options: LaunchOptions = {})
|
||||
const client = await connectWithRetry(socketPath, 3, 1000)
|
||||
|
||||
try {
|
||||
// Build MCP config (approvals enabled by default unless explicitly disabled)
|
||||
// Phase 6: Using HTTP MCP endpoint instead of stdio
|
||||
const daemonPort = process.env.HUMANLAYER_DAEMON_HTTP_PORT || '7777'
|
||||
const mcpConfig =
|
||||
options.approvals !== false
|
||||
? {
|
||||
mcpServers: {
|
||||
codelayer: {
|
||||
type: 'http',
|
||||
url: `http://localhost:${daemonPort}/api/v1/mcp`,
|
||||
// Session ID will be added as header by Claude Code
|
||||
},
|
||||
},
|
||||
}
|
||||
: undefined
|
||||
|
||||
// Launch the session
|
||||
const result = await client.launchSession({
|
||||
query: query,
|
||||
@@ -68,8 +52,9 @@ export const launchCommand = async (query: string, options: LaunchOptions = {})
|
||||
model: options.model,
|
||||
working_dir: options.workingDir || process.cwd(),
|
||||
max_turns: options.maxTurns,
|
||||
mcp_config: mcpConfig,
|
||||
permission_prompt_tool: mcpConfig ? 'mcp__codelayer__request_approval' : undefined,
|
||||
// MCP config is now injected by daemon
|
||||
permission_prompt_tool:
|
||||
options.approvals !== false ? 'mcp__codelayer__request_permission' : undefined,
|
||||
dangerously_skip_permissions: options.dangerouslySkipPermissions,
|
||||
dangerously_skip_permissions_timeout: options.dangerouslySkipPermissionsTimeout
|
||||
? parseInt(options.dangerouslySkipPermissionsTimeout) * 60 * 1000
|
||||
|
||||
@@ -353,11 +353,13 @@ export class DaemonClient extends EventEmitter {
|
||||
runId: string,
|
||||
toolName: string,
|
||||
toolInput: unknown,
|
||||
toolUseId?: string,
|
||||
): Promise<{ approval_id: string }> {
|
||||
return this.call<{ approval_id: string }>('createApproval', {
|
||||
run_id: runId,
|
||||
tool_name: toolName,
|
||||
tool_input: toolInput,
|
||||
...(toolUseId && { tool_use_id: toolUseId }),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { launchCommand } from './commands/launch.js'
|
||||
import { alertCommand } from './commands/alert.js'
|
||||
import { thoughtsCommand } from './commands/thoughts.js'
|
||||
import { joinWaitlistCommand } from './commands/joinWaitlist.js'
|
||||
import { startClaudeApprovalsMCPServer } from './mcp.js'
|
||||
import {
|
||||
getDefaultConfigPath,
|
||||
resolveFullConfig,
|
||||
@@ -66,7 +67,7 @@ async function authenticate(printSelectedProject: boolean = false) {
|
||||
|
||||
program.name('humanlayer').description('HumanLayer, but on your command-line.').version(VERSION)
|
||||
|
||||
const UNPROTECTED_COMMANDS = ['config', 'login', 'thoughts', 'join-waitlist', 'launch']
|
||||
const UNPROTECTED_COMMANDS = ['config', 'login', 'thoughts', 'join-waitlist', 'launch', 'mcp']
|
||||
|
||||
program.hook('preAction', async (thisCmd, actionCmd) => {
|
||||
// Get the full command path by traversing up the command hierarchy
|
||||
@@ -96,6 +97,13 @@ program
|
||||
.option('--config-file <path>', 'Path to config file')
|
||||
.action(loginCommand)
|
||||
|
||||
const mcpCommand = program.command('mcp').description('MCP server functionality')
|
||||
|
||||
mcpCommand
|
||||
.command('claude_approvals')
|
||||
.description('Start the Claude approvals MCP server for permission requests')
|
||||
.action(startClaudeApprovalsMCPServer)
|
||||
|
||||
program
|
||||
.command('launch <query>')
|
||||
.description('Launch a new Claude Code session via the daemon')
|
||||
|
||||
192
hlyr/src/mcp.ts
Normal file
192
hlyr/src/mcp.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ErrorCode,
|
||||
ListToolsRequestSchema,
|
||||
McpError,
|
||||
} from '@modelcontextprotocol/sdk/types.js'
|
||||
import { resolveFullConfig } from './config.js'
|
||||
import { DaemonClient } from './daemonClient.js'
|
||||
import { logger } from './mcpLogger.js'
|
||||
|
||||
/**
|
||||
* Start the Claude approvals MCP server that provides request_permission functionality
|
||||
* Returns responses in the format required by Claude Code SDK
|
||||
*
|
||||
* This uses local approvals through the daemon instead of HumanLayer API
|
||||
*/
|
||||
export async function startClaudeApprovalsMCPServer() {
|
||||
// No auth validation needed - uses local daemon
|
||||
logger.info('Starting Claude approvals MCP server')
|
||||
logger.info('Environment variables', {
|
||||
HUMANLAYER_DAEMON_SOCKET: process.env.HUMANLAYER_DAEMON_SOCKET,
|
||||
HUMANLAYER_SESSION_ID: process.env.HUMANLAYER_SESSION_ID,
|
||||
})
|
||||
|
||||
const server = new Server(
|
||||
{
|
||||
name: 'humanlayer-claude-local-approvals',
|
||||
version: '1.0.0',
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
// Create daemon client with socket path from environment or config
|
||||
// The daemon sets HUMANLAYER_DAEMON_SOCKET for MCP servers it launches
|
||||
const resolvedConfig = resolveFullConfig({})
|
||||
const socketPath = process.env.HUMANLAYER_DAEMON_SOCKET || resolvedConfig.daemon_socket
|
||||
logger.info('Creating daemon client', { socketPath })
|
||||
const daemonClient = new DaemonClient(socketPath)
|
||||
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
logger.info('ListTools request received')
|
||||
const tools = [
|
||||
{
|
||||
name: 'request_permission',
|
||||
description: 'Request permission to perform an action',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
tool_name: { type: 'string' },
|
||||
input: { type: 'object' },
|
||||
tool_use_id: { type: 'string' }, // Added for Phase 2
|
||||
},
|
||||
required: ['tool_name', 'input', 'tool_use_id'],
|
||||
},
|
||||
},
|
||||
]
|
||||
logger.info('Returning tools', { tools })
|
||||
return { tools }
|
||||
})
|
||||
|
||||
server.setRequestHandler(CallToolRequestSchema, async request => {
|
||||
logger.debug('Received tool call request', { name: request.params.name })
|
||||
|
||||
if (request.params.name === 'request_permission') {
|
||||
const toolName: string | undefined = request.params.arguments?.tool_name
|
||||
const toolUseId: string | undefined = request.params.arguments?.tool_use_id // Phase 2
|
||||
|
||||
if (!toolName) {
|
||||
logger.error('Invalid tool name in request_permission', request.params.arguments)
|
||||
throw new McpError(ErrorCode.InvalidRequest, 'Invalid tool name requesting permissions')
|
||||
}
|
||||
|
||||
const input: Record<string, unknown> = request.params.arguments?.input || {}
|
||||
|
||||
// Get session ID from environment (set by daemon)
|
||||
const sessionId = process.env.HUMANLAYER_SESSION_ID
|
||||
if (!sessionId) {
|
||||
logger.error('HUMANLAYER_SESSION_ID not set in environment')
|
||||
throw new McpError(ErrorCode.InternalError, 'HUMANLAYER_SESSION_ID not set')
|
||||
}
|
||||
|
||||
logger.info('Processing approval request', { sessionId, toolName, toolUseId })
|
||||
|
||||
try {
|
||||
// Connect to daemon
|
||||
logger.debug('Connecting to daemon...')
|
||||
await daemonClient.connect()
|
||||
logger.debug('Connected to daemon')
|
||||
|
||||
// Create approval request with tool use ID (Phase 2)
|
||||
logger.debug('Creating approval request...', { sessionId, toolName, toolUseId })
|
||||
const createResponse = await daemonClient.createApproval(sessionId, toolName, input, toolUseId)
|
||||
const approvalId = createResponse.approval_id
|
||||
logger.info('Created approval', { approvalId })
|
||||
|
||||
// Poll for approval status
|
||||
let approved = false
|
||||
let comment = ''
|
||||
let polling = true
|
||||
|
||||
while (polling) {
|
||||
try {
|
||||
// Get the specific approval by ID
|
||||
logger.debug('Fetching approval status...', { approvalId })
|
||||
const approval = (await daemonClient.getApproval(approvalId)) as {
|
||||
id: string
|
||||
status: string
|
||||
comment?: string
|
||||
}
|
||||
|
||||
logger.debug('Approval status', { status: approval.status })
|
||||
|
||||
if (approval.status !== 'pending') {
|
||||
// Approval has been resolved
|
||||
approved = approval.status === 'approved'
|
||||
comment = approval.comment || ''
|
||||
polling = false
|
||||
logger.info('Approval resolved', {
|
||||
approvalId,
|
||||
status: approval.status,
|
||||
approved,
|
||||
})
|
||||
} else {
|
||||
// Still pending, wait and poll again
|
||||
logger.debug('Approval still pending, polling again...')
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to get approval status', { error, approvalId })
|
||||
// Re-throw the error since this is a critical failure
|
||||
throw new McpError(ErrorCode.InternalError, 'Failed to get approval status')
|
||||
}
|
||||
}
|
||||
|
||||
if (!approved) {
|
||||
logger.info('Approval denied', { approvalId, comment })
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify({
|
||||
behavior: 'deny',
|
||||
message: comment || 'Request denied by human reviewer',
|
||||
}),
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
logger.info('Approval granted', { approvalId })
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify({
|
||||
behavior: 'allow',
|
||||
updatedInput: input,
|
||||
}),
|
||||
},
|
||||
],
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to process approval', error)
|
||||
throw new McpError(
|
||||
ErrorCode.InternalError,
|
||||
`Failed to process approval: ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
} finally {
|
||||
logger.debug('Closing daemon connection')
|
||||
daemonClient.close()
|
||||
}
|
||||
}
|
||||
|
||||
throw new McpError(ErrorCode.InvalidRequest, 'Invalid tool name')
|
||||
})
|
||||
|
||||
const transport = new StdioServerTransport()
|
||||
|
||||
try {
|
||||
await server.connect(transport)
|
||||
logger.info('MCP server connected and ready')
|
||||
} catch (error) {
|
||||
logger.error('Failed to start MCP server', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { create } from 'zustand'
|
||||
import { daemonClient } from '@/lib/daemon'
|
||||
import type { LaunchSessionRequest } from '@/lib/daemon/types'
|
||||
import { getDaemonUrl } from '@/lib/daemon/http-config'
|
||||
import { useHotkeysContext } from 'react-hotkeys-hook'
|
||||
import { SessionTableHotkeysScope } from '@/components/internal/SessionTable'
|
||||
import { exists } from '@tauri-apps/plugin-fs'
|
||||
@@ -141,17 +140,7 @@ export const useSessionLauncher = create<LauncherState>((set, get) => ({
|
||||
try {
|
||||
set({ isLaunching: true, error: undefined })
|
||||
|
||||
// Build MCP config (approvals enabled by default)
|
||||
// Use HTTP-based MCP server built into the daemon
|
||||
const daemonUrl = await getDaemonUrl()
|
||||
const mcpConfig = {
|
||||
mcpServers: {
|
||||
approvals: {
|
||||
type: 'http',
|
||||
url: `${daemonUrl}/api/v1/mcp`,
|
||||
},
|
||||
},
|
||||
}
|
||||
// MCP config is now injected by daemon
|
||||
|
||||
const request: LaunchSessionRequest = {
|
||||
query: query.trim(),
|
||||
@@ -159,8 +148,8 @@ export const useSessionLauncher = create<LauncherState>((set, get) => ({
|
||||
working_dir: config.workingDir || undefined,
|
||||
model: config.model || undefined,
|
||||
max_turns: config.maxTurns || undefined,
|
||||
mcp_config: mcpConfig,
|
||||
permission_prompt_tool: 'mcp__approvals__request_approval',
|
||||
// MCP config is now injected by daemon
|
||||
permission_prompt_tool: 'mcp__codelayer__request_permission',
|
||||
}
|
||||
|
||||
const response = await daemonClient.launchSession(request)
|
||||
|
||||
Reference in New Issue
Block a user