@@ -22,6 +22,7 @@ import (
2222const (
2323 defaultCommandTimeout = 30 * time .Second
2424 dependencyInstallTimeout = 5 * time .Minute
25+ maxLogTailLines = 10000 // Maximum number of log lines to retrieve
2526)
2627
2728// Command constants
@@ -31,7 +32,6 @@ const (
3132 jsonOutputFlag = "--output"
3233 jsonOutputVal = "json"
3334 cwdFlag = "--cwd"
34- projectFlag = "--project"
3535)
3636
3737// Allowed values for validation
@@ -86,13 +86,19 @@ This server complements azd's core MCP capabilities:
86861. Always use get_services to check current state before starting/stopping services
87872. Use check_requirements before installing dependencies to see what's needed
88883. Use get_service_logs to diagnose issues when services fail to start
89- 4. Read azure.yaml resource to understand project structure before operations
89+ 4. Read azure://project/azure .yaml resource to understand project structure before operations
9090
9191**Tool Categories:**
9292- Observability: get_services, get_service_logs, get_project_info
93- - Operations: run_services, stop_services, restart_service, install_dependencies
93+ - Operations: run_services, stop_services, start_service, restart_service, install_dependencies
9494- Configuration: check_requirements, get_environment_variables, set_environment_variable
9595
96+ **Service Lifecycle:**
97+ - run_services: Start all services (background process, use get_services to check status)
98+ - start_service: Start a specific stopped service
99+ - stop_services: Stop all or a specific running service (graceful shutdown)
100+ - restart_service: Stop and start a specific service
101+
96102**Integration Notes:**
97103- Works with projects created by azd init or azd templates
98104- Monitors services started by azd app run
@@ -101,7 +107,7 @@ This server complements azd's core MCP capabilities:
101107 // Create MCP server with all capabilities
102108 // Server name follows azd extension naming convention: {namespace}-mcp-server
103109 s := server .NewMCPServer (
104- "app-mcp-server" , "0.1.0" ,
110+ "app-mcp-server" , Version ,
105111 server .WithToolCapabilities (true ),
106112 server .WithResourceCapabilities (false , true ), // subscribe=false, listChanged=true
107113 server .WithPromptCapabilities (false ), // listChanged=false
@@ -117,6 +123,7 @@ This server complements azd's core MCP capabilities:
117123 // Operational tools
118124 newRunServicesTool (),
119125 newStopServicesTool (),
126+ newStartServiceTool (),
120127 newRestartServiceTool (),
121128 newInstallDependenciesTool (),
122129 newCheckRequirementsTool (),
@@ -234,6 +241,20 @@ func extractProjectDirArg(args map[string]interface{}) ([]string, error) {
234241 return cmdArgs , nil
235242}
236243
244+ // extractValidatedProjectDir extracts and validates projectDir from args, returning the path.
245+ // Falls back to getProjectDir() if not provided in args.
246+ func extractValidatedProjectDir (args map [string ]interface {}) (string , error ) {
247+ projectDir := getProjectDir ()
248+ if pd , ok := getStringParam (args , "projectDir" ); ok {
249+ validatedPath , err := validateProjectDir (pd )
250+ if err != nil {
251+ return "" , err
252+ }
253+ projectDir = validatedPath
254+ }
255+ return projectDir , nil
256+ }
257+
237258// validateRequiredParam validates that a required parameter exists and returns the value
238259func validateRequiredParam (args map [string ]interface {}, key string ) (string , error ) {
239260 val , ok := args [key ].(string )
@@ -455,8 +476,8 @@ func newGetServiceLogsTool() server.ServerTool {
455476
456477 if tail , ok := getFloat64Param (args , "tail" ); ok && tail > 0 {
457478 // Cap tail at reasonable maximum
458- if tail > 10000 {
459- tail = 10000
479+ if tail > float64 ( maxLogTailLines ) {
480+ tail = float64 ( maxLogTailLines )
460481 }
461482 cmdArgs = append (cmdArgs , "--tail" , fmt .Sprintf ("%.0f" , tail ))
462483 }
@@ -746,15 +767,94 @@ func newStopServicesTool() server.ServerTool {
746767 mcp .WithString ("projectDir" ,
747768 mcp .Description ("Optional project directory path. If not provided, uses current directory." ),
748769 ),
770+ mcp .WithString ("serviceName" ,
771+ mcp .Description ("Optional specific service to stop. If not provided, stops all running services." ),
772+ ),
749773 ),
750774 Handler : func (ctx context.Context , request mcp.CallToolRequest ) (* mcp.CallToolResult , error ) {
751- // Note: azd app doesn't have a direct stop command, so we provide guidance
752- result := map [string ]interface {}{
753- "status" : "info" ,
754- "message" : "To stop services, use Ctrl+C in the terminal running 'azd app run', or use system tools to kill the process." ,
755- "tip" : "You can use get_services to find the PID of running services." ,
775+ args := getArgsMap (request )
776+
777+ // Get project directory
778+ projectDir , err := extractValidatedProjectDir (args )
779+ if err != nil {
780+ return mcp .NewToolResultError (fmt .Sprintf ("Invalid project directory: %v" , err )), nil
781+ }
782+
783+ // Create service controller
784+ ctrl , err := NewServiceController (projectDir )
785+ if err != nil {
786+ return mcp .NewToolResultError (fmt .Sprintf ("Failed to initialize service controller: %v" , err )), nil
787+ }
788+
789+ // Check if a specific service was requested
790+ if serviceName , ok := getStringParam (args , "serviceName" ); ok {
791+ if err := security .ValidateServiceName (serviceName , false ); err != nil {
792+ return mcp .NewToolResultError (err .Error ()), nil
793+ }
794+ result := ctrl .StopService (ctx , serviceName )
795+ return marshalToolResult (result )
796+ }
797+
798+ // Stop all running services
799+ runningServices := ctrl .GetRunningServices ()
800+ if len (runningServices ) == 0 {
801+ return marshalToolResult (BulkServiceControlResult {
802+ Success : true ,
803+ Message : "No running services to stop" ,
804+ Results : []ServiceControlResult {},
805+ })
806+ }
807+
808+ result := ctrl .BulkStop (ctx , runningServices )
809+ return marshalToolResult (result )
810+ },
811+ }
812+ }
813+
814+ // newStartServiceTool creates the start_service tool
815+ func newStartServiceTool () server.ServerTool {
816+ return server.ServerTool {
817+ Tool : mcp .NewTool (
818+ "start_service" ,
819+ mcp .WithTitleAnnotation ("Start Service" ),
820+ mcp .WithDescription ("Start a specific stopped service. Use this to start individual services that were previously stopped." ),
821+ mcp .WithReadOnlyHintAnnotation (false ),
822+ mcp .WithIdempotentHintAnnotation (false ),
823+ mcp .WithDestructiveHintAnnotation (false ),
824+ mcp .WithString ("serviceName" ,
825+ mcp .Description ("Name of the service to start" ),
826+ mcp .Required (),
827+ ),
828+ mcp .WithString ("projectDir" ,
829+ mcp .Description ("Optional project directory path. If not provided, uses current directory." ),
830+ ),
831+ ),
832+ Handler : func (ctx context.Context , request mcp.CallToolRequest ) (* mcp.CallToolResult , error ) {
833+ args := getArgsMap (request )
834+
835+ serviceName , err := validateRequiredParam (args , "serviceName" )
836+ if err != nil {
837+ return mcp .NewToolResultError (err .Error ()), nil
756838 }
757839
840+ // Validate service name to prevent injection
841+ if err := security .ValidateServiceName (serviceName , false ); err != nil {
842+ return mcp .NewToolResultError (err .Error ()), nil
843+ }
844+
845+ // Get project directory
846+ projectDir , err := extractValidatedProjectDir (args )
847+ if err != nil {
848+ return mcp .NewToolResultError (fmt .Sprintf ("Invalid project directory: %v" , err )), nil
849+ }
850+
851+ // Create service controller
852+ ctrl , err := NewServiceController (projectDir )
853+ if err != nil {
854+ return mcp .NewToolResultError (fmt .Sprintf ("Failed to initialize service controller: %v" , err )), nil
855+ }
856+
857+ result := ctrl .StartService (ctx , serviceName )
758858 return marshalToolResult (result )
759859 },
760860 }
@@ -791,12 +891,19 @@ func newRestartServiceTool() server.ServerTool {
791891 return mcp .NewToolResultError (err .Error ()), nil
792892 }
793893
794- result := map [string ]interface {}{
795- "status" : "info" ,
796- "message" : fmt .Sprintf ("To restart service '%s', first stop it (Ctrl+C or kill PID), then use run_services to start it again." , serviceName ),
797- "tip" : "Use get_services to find the current PID of the service." ,
894+ // Get project directory
895+ projectDir , err := extractValidatedProjectDir (args )
896+ if err != nil {
897+ return mcp .NewToolResultError (fmt .Sprintf ("Invalid project directory: %v" , err )), nil
898+ }
899+
900+ // Create service controller
901+ ctrl , err := NewServiceController (projectDir )
902+ if err != nil {
903+ return mcp .NewToolResultError (fmt .Sprintf ("Failed to initialize service controller: %v" , err )), nil
798904 }
799905
906+ result := ctrl .RestartService (ctx , serviceName )
800907 return marshalToolResult (result )
801908 },
802909 }
0 commit comments