From 32fe3a5a6f5cf4cb77ee9c0da647d79297948193 Mon Sep 17 00:00:00 2001 From: suiramdev Date: Sun, 3 Aug 2025 00:28:03 +0200 Subject: [PATCH 1/8] feat: use HTTP server transport --- Program.cs | 57 ++++++-- Resources/.gitkeep | 0 Services/EditorResourceService.cs | 110 ++++++++++++++ Services/Interfaces/IResourceService.cs | 10 ++ ...{IEditorToolService.cs => IToolService.cs} | 4 +- Services/Models/CallResourceRequest.cs | 21 +++ Services/Models/CallResourceResponse.cs | 23 +++ ...ditorToolRequest.cs => CallToolRequest.cs} | 7 +- ...torToolResponse.cs => CallToolResponse.cs} | 7 +- .../{EditorToolService.cs => ToolService.cs} | 43 +++--- Services/WebSocketService.cs | 6 +- Tools/EditorSceneTool.cs | 12 +- Tools/EditorSessionTool.cs | 30 ++-- Tools/GameObjectTool.cs | 136 +++++++++--------- appsettings.json | 6 - 15 files changed, 330 insertions(+), 142 deletions(-) create mode 100644 Resources/.gitkeep create mode 100644 Services/EditorResourceService.cs create mode 100644 Services/Interfaces/IResourceService.cs rename Services/Interfaces/{IEditorToolService.cs => IToolService.cs} (64%) create mode 100644 Services/Models/CallResourceRequest.cs create mode 100644 Services/Models/CallResourceResponse.cs rename Services/Models/{CallEditorToolRequest.cs => CallToolRequest.cs} (81%) rename Services/Models/{CallEditorToolResponse.cs => CallToolResponse.cs} (83%) rename Services/{EditorToolService.cs => ToolService.cs} (68%) delete mode 100644 appsettings.json diff --git a/Program.cs b/Program.cs index 1a2cfbf..92e6747 100644 --- a/Program.cs +++ b/Program.cs @@ -2,9 +2,12 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; using SandboxModelContextProtocol.Server.Services; using SandboxModelContextProtocol.Server.Services.Interfaces; using SandboxModelContextProtocol.Server.Services.Models; +using Microsoft.Extensions.Configuration; namespace SandboxModelContextProtocol.Server; @@ -12,33 +15,59 @@ public class Program { public static async Task Main( string[] args ) { - var builder = Host.CreateApplicationBuilder( args ); + var builder = WebApplication.CreateBuilder( args ); - // Configure logging to go to stderr for MCP protocol compliance - builder.Logging.AddConsole( consoleLogOptions => - { - consoleLogOptions.LogToStandardErrorThreshold = LogLevel.Trace; - } ); + builder.WebHost.UseUrls( "http://localhost:8080" ); + + // Configure logging for HTTP transport + builder.Logging.AddConsole(); // Configure WebSocket options - builder.Services.Configure( + builder.Services.Configure( builder.Configuration.GetSection( "WebSocket" ) ); // Register the command service - builder.Services.AddSingleton(); + builder.Services.AddSingleton(); - // Configure MCP Server with stdio transport and tools from assembly + // Register the resource service + builder.Services.AddSingleton(); + + // Configure MCP Server with HTTP transport, tools and resources from assembly builder.Services .AddMcpServer() - .WithStdioServerTransport() - .WithToolsFromAssembly(); + .WithHttpTransport() + .WithToolsFromAssembly() + .WithResourcesFromAssembly(); - // Register WebSocket service as both hosted service and singleton for interface + // Register WebSocket service as singleton (no longer a hosted service) builder.Services.AddSingleton(); builder.Services.AddSingleton( provider => provider.GetRequiredService() ); - builder.Services.AddHostedService( provider => provider.GetRequiredService() ); - await builder.Build().RunAsync(); + var app = builder.Build(); + + // Enable WebSocket support + app.UseWebSockets(); + + // Map MCP endpoints for HTTP transport + app.MapMcp(); + + // Configure WebSocket endpoint + var webSocketService = app.Services.GetRequiredService(); + + app.Map( "/ws", async context => + { + if ( context.WebSockets.IsWebSocketRequest ) + { + using var webSocket = await context.WebSockets.AcceptWebSocketAsync(); + await webSocketService.HandleWebSocketConnection( webSocket, context.RequestAborted ); + } + else + { + context.Response.StatusCode = 400; + } + } ); + + await app.RunAsync(); } } diff --git a/Resources/.gitkeep b/Resources/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/Services/EditorResourceService.cs b/Services/EditorResourceService.cs new file mode 100644 index 0000000..4f5544d --- /dev/null +++ b/Services/EditorResourceService.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Concurrent; +using System.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using SandboxModelContextProtocol.Server.Services.Interfaces; +using SandboxModelContextProtocol.Server.Services.Models; + +namespace SandboxModelContextProtocol.Server.Services; + +public class ResourceService( ILogger logger, IServiceProvider serviceProvider ) : IResourceService +{ + private readonly ILogger _logger = logger; + private readonly ConcurrentDictionary> _pendingRequests = new(); + private readonly IWebSocketService _webSocketService = serviceProvider.GetRequiredService(); + + public async Task GetResource( CallResourceRequest request ) + { + _logger.LogInformation( "Getting resource: {Name}", request.Name ); + + // Find active connections + var activeConnections = _webSocketService.GetWebSocketConnections() + .Where( conn => conn.IsConnected ) + .ToList(); + + // If no active connections, return an error + if ( activeConnections.Count == 0 ) + { + return new CallResourceResponse() + { + Id = request.Id, + Name = request.Name, + Content = [JsonSerializer.SerializeToElement( "No active s&box connections available" )], + IsError = true + }; + } + + // Add command ID to request + var requestJson = JsonSerializer.Serialize( request ); + + // Create task completion source for this request + var tcs = new TaskCompletionSource(); + _pendingRequests[request.Id] = tcs; + + try + { + // Send request to all active s&box connections + await _webSocketService.SendToAll( requestJson ); + + _logger.LogInformation( "Resource request sent to s&box connections" ); + + // Wait for response with timeout + using var cts = new CancellationTokenSource( TimeSpan.FromSeconds( 30 ) ); + cts.Token.Register( () => + { + if ( tcs.TrySetCanceled() ) + { + _pendingRequests.TryRemove( request.Id, out _ ); + _logger.LogWarning( "Resource request {Id} timed out", request.Id ); + } + } ); + + return await tcs.Task; + } + catch ( OperationCanceledException ) + { + _pendingRequests.TryRemove( request.Id, out _ ); + return new CallResourceResponse() + { + Id = request.Id, + Name = request.Name, + Content = [JsonSerializer.SerializeToElement( "Resource request timed out after 30 seconds" )], + IsError = true + }; + } + catch ( Exception ex ) + { + _pendingRequests.TryRemove( request.Id, out _ ); + _logger.LogError( ex, "Failed to send resource request to s&box connections" ); + return new CallResourceResponse() + { + Id = request.Id, + Name = request.Name, + Content = [JsonSerializer.SerializeToElement( $"Failed to send resource request: {ex.Message}" )], + IsError = true + }; + } + } + + public void HandleResponse( string message ) + { + _logger.LogInformation( "Handling resource response: {Message}", message ); + + CallResourceResponse? response = JsonSerializer.Deserialize( message ); + if ( response == null ) + { + _logger.LogWarning( "Failed to parse resource response JSON: {Message}", message ); + return; + } + + if ( _pendingRequests.TryRemove( response.Id, out var tcs ) ) + { + tcs.SetResult( response ); + _logger.LogInformation( "Resource request {Id} completed successfully", response.Id ); + } + } +} diff --git a/Services/Interfaces/IResourceService.cs b/Services/Interfaces/IResourceService.cs new file mode 100644 index 0000000..8811c79 --- /dev/null +++ b/Services/Interfaces/IResourceService.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using SandboxModelContextProtocol.Server.Services.Models; + +namespace SandboxModelContextProtocol.Server.Services.Interfaces; + +public interface IResourceService +{ + Task GetResource( CallResourceRequest request ); + void HandleResponse( string message ); +} diff --git a/Services/Interfaces/IEditorToolService.cs b/Services/Interfaces/IToolService.cs similarity index 64% rename from Services/Interfaces/IEditorToolService.cs rename to Services/Interfaces/IToolService.cs index 42b3add..d14bd4b 100644 --- a/Services/Interfaces/IEditorToolService.cs +++ b/Services/Interfaces/IToolService.cs @@ -3,8 +3,8 @@ namespace SandboxModelContextProtocol.Server.Services.Interfaces; -public interface IEditorToolService +public interface IToolService { - Task CallTool( CallEditorToolRequest request ); + Task CallTool( CallToolRequest request ); void HandleResponse( string response ); } diff --git a/Services/Models/CallResourceRequest.cs b/Services/Models/CallResourceRequest.cs new file mode 100644 index 0000000..e974966 --- /dev/null +++ b/Services/Models/CallResourceRequest.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace SandboxModelContextProtocol.Server.Services.Models; + +public class CallResourceRequest +{ + [JsonIgnore] + public string Type { get; set; } = "resource"; + + [JsonPropertyName( "id" )] + public string Id { get; set; } = Guid.NewGuid().ToString(); + + [JsonPropertyName( "name" )] + public required string Name { get; init; } + + [JsonPropertyName( "arguments" )] + public Dictionary Arguments { get; set; } = new(); +} diff --git a/Services/Models/CallResourceResponse.cs b/Services/Models/CallResourceResponse.cs new file mode 100644 index 0000000..303f346 --- /dev/null +++ b/Services/Models/CallResourceResponse.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace SandboxModelContextProtocol.Server.Services.Models; + +public class CallResourceResponse +{ + [JsonPropertyName( "type" )] + public string Type { get; } = "resource"; + + [JsonPropertyName( "id" )] + public required string Id { get; set; } + + [JsonPropertyName( "name" )] + public required string Name { get; set; } + + [JsonPropertyName( "content" )] + public List Content { get; set; } = []; + + [JsonPropertyName( "isError" )] + public bool IsError { get; set; } +} diff --git a/Services/Models/CallEditorToolRequest.cs b/Services/Models/CallToolRequest.cs similarity index 81% rename from Services/Models/CallEditorToolRequest.cs rename to Services/Models/CallToolRequest.cs index 4da10ed..9f2e846 100644 --- a/Services/Models/CallEditorToolRequest.cs +++ b/Services/Models/CallToolRequest.cs @@ -5,8 +5,11 @@ namespace SandboxModelContextProtocol.Server.Services.Models; -public class CallEditorToolRequest +public class CallToolRequest { + [JsonPropertyName( "type" )] + public string Type { get; } = "tool"; + [JsonPropertyName( "id" )] public string Id { get; set; } = Guid.NewGuid().ToString(); @@ -15,4 +18,4 @@ public class CallEditorToolRequest [JsonPropertyName( "arguments" )] public IReadOnlyDictionary? Arguments { get; init; } -} \ No newline at end of file +} diff --git a/Services/Models/CallEditorToolResponse.cs b/Services/Models/CallToolResponse.cs similarity index 83% rename from Services/Models/CallEditorToolResponse.cs rename to Services/Models/CallToolResponse.cs index aed75e6..b15f523 100644 --- a/Services/Models/CallEditorToolResponse.cs +++ b/Services/Models/CallToolResponse.cs @@ -4,8 +4,11 @@ namespace SandboxModelContextProtocol.Server.Services.Models; -public class CallEditorToolResponse +public class CallToolResponse { + [JsonIgnore] + public string Type { get; set; } = "tool"; + [JsonPropertyName( "id" )] public required string Id { get; set; } @@ -17,4 +20,4 @@ public class CallEditorToolResponse [JsonPropertyName( "isError" )] public bool IsError { get; set; } -} \ No newline at end of file +} diff --git a/Services/EditorToolService.cs b/Services/ToolService.cs similarity index 68% rename from Services/EditorToolService.cs rename to Services/ToolService.cs index e39ba5a..db3631d 100644 --- a/Services/EditorToolService.cs +++ b/Services/ToolService.cs @@ -11,17 +11,14 @@ namespace SandboxModelContextProtocol.Server.Services; -public class EditorToolService( ILogger logger, IServiceProvider serviceProvider ) : IEditorToolService +public class ToolService( ILogger logger, IServiceProvider serviceProvider ) : IToolService { - private readonly ILogger _logger = logger; - private readonly ConcurrentDictionary> _pendingCommands = new(); + private readonly ILogger _logger = logger; + private readonly ConcurrentDictionary> _pendingCommands = new(); private readonly IWebSocketService _webSocketService = serviceProvider.GetRequiredService(); - public async Task CallTool( CallEditorToolRequest request ) + public async Task CallTool( CallToolRequest request ) { - // Generate unique command ID - var id = Guid.NewGuid().ToString(); - _logger.LogInformation( "Executing tool call: {Name}", request.Name ); // Find active connections @@ -32,27 +29,25 @@ public async Task CallTool( CallEditorToolRequest reques // If no active connections, return an error if ( activeConnections.Count == 0 ) { - return new CallEditorToolResponse() + return new CallToolResponse() { - Id = id, + Id = request.Id, Name = request.Name, Content = [JsonSerializer.SerializeToElement( "No active s&box connections available" )], IsError = true }; } - // Add command ID to request - request.Id = id; - var commandJson = JsonSerializer.Serialize( request ); + var requestJson = JsonSerializer.Serialize( request ); // Create task completion source for this command - var tcs = new TaskCompletionSource(); - _pendingCommands[id] = tcs; + var tcs = new TaskCompletionSource(); + _pendingCommands[request.Id] = tcs; try { // Send command to all active s&box connections - await _webSocketService.SendToAll( commandJson ); + await _webSocketService.SendToAll( requestJson ); _logger.LogInformation( "Tool call sent to s&box connections" ); @@ -62,8 +57,8 @@ public async Task CallTool( CallEditorToolRequest reques { if ( tcs.TrySetCanceled() ) { - _pendingCommands.TryRemove( id, out _ ); - _logger.LogWarning( "Tool call {Id} timed out", id ); + _pendingCommands.TryRemove( request.Id, out _ ); + _logger.LogWarning( "Tool call {Id} timed out", request.Id ); } } ); @@ -71,10 +66,10 @@ public async Task CallTool( CallEditorToolRequest reques } catch ( OperationCanceledException ) { - _pendingCommands.TryRemove( id, out _ ); - return new CallEditorToolResponse() + _pendingCommands.TryRemove( request.Id, out _ ); + return new CallToolResponse() { - Id = id, + Id = request.Id, Name = request.Name, Content = [JsonSerializer.SerializeToElement( "Tool call timed out after 30 seconds" )], IsError = true @@ -82,11 +77,11 @@ public async Task CallTool( CallEditorToolRequest reques } catch ( Exception ex ) { - _pendingCommands.TryRemove( id, out _ ); + _pendingCommands.TryRemove( request.Id, out _ ); _logger.LogError( ex, "Failed to send tool call to s&box connections" ); - return new CallEditorToolResponse() + return new CallToolResponse() { - Id = id, + Id = request.Id, Name = request.Name, Content = [JsonSerializer.SerializeToElement( $"Failed to send tool call: {ex.Message}" )], IsError = true @@ -98,7 +93,7 @@ public void HandleResponse( string message ) { _logger.LogInformation( "Handling response: {Message}", message ); - CallEditorToolResponse? response = JsonSerializer.Deserialize( message ); + CallToolResponse? response = JsonSerializer.Deserialize( message ); if ( response == null ) { _logger.LogWarning( "Failed to parse response JSON: {Message}", message ); diff --git a/Services/WebSocketService.cs b/Services/WebSocketService.cs index 50438e4..c77940e 100644 --- a/Services/WebSocketService.cs +++ b/Services/WebSocketService.cs @@ -61,7 +61,7 @@ public async Task StartAsync( CancellationToken cancellationToken ) } ); await _app.StartAsync( cancellationToken ); - _logger.LogInformation( "WebSocket Server started on {WebSocketUrl}{WebSocketPath}", _options.Url, _options.Path ); + _logger.LogInformation( "WebSocket Server started on {WebSocketPath}", _options.Path ); } /// @@ -123,7 +123,7 @@ private void UnregisterWebSocketConnection( WebSocketConnection connection ) _logger.LogInformation( "WebSocket connection unregistered: {ConnectionId}", connectionId ); } - private async Task HandleWebSocketConnection( WebSocket webSocket, CancellationToken cancellationToken ) + public async Task HandleWebSocketConnection( WebSocket webSocket, CancellationToken cancellationToken ) { var connection = new WebSocketConnection( webSocket, _logger ); RegisterWebSocketConnection( connection ); @@ -151,7 +151,7 @@ private async Task HandleWebSocketConnection( WebSocket webSocket, CancellationT _logger.LogInformation( "Received from s&box: {Message}", message ); // Handle responses from s&box - var commandService = _serviceProvider.GetRequiredService(); + var commandService = _serviceProvider.GetRequiredService(); commandService.HandleResponse( message ); } else if ( result.MessageType == WebSocketMessageType.Close ) diff --git a/Tools/EditorSceneTool.cs b/Tools/EditorSceneTool.cs index 2cf0e11..ac96361 100644 --- a/Tools/EditorSceneTool.cs +++ b/Tools/EditorSceneTool.cs @@ -9,14 +9,14 @@ namespace SandboxModelContextProtocol.Server.Tools; [McpServerToolType] -public class EditorSceneTool( IEditorToolService editorToolService ) +public class EditorSceneTool( IToolService editorToolService ) { - private readonly IEditorToolService _editorToolService = editorToolService; + private readonly IToolService _editorToolService = editorToolService; [McpServerTool, Description( "Gets the scene in the active editor session." )] - public async Task GetActiveEditorScene() + public async Task GetActiveEditorScene() { - var command = new CallEditorToolRequest() + var command = new CallToolRequest() { Name = nameof( GetActiveEditorScene ), }; @@ -25,9 +25,9 @@ public async Task GetActiveEditorScene() } [McpServerTool, Description( "Loads a scene in a new editor session from a path." )] - public async Task LoadEditorSceneFromPath( string path ) + public async Task LoadEditorSceneFromPath( string path ) { - var command = new CallEditorToolRequest() + var command = new CallToolRequest() { Name = nameof( LoadEditorSceneFromPath ), Arguments = new Dictionary() diff --git a/Tools/EditorSessionTool.cs b/Tools/EditorSessionTool.cs index 96f248f..9cf6270 100644 --- a/Tools/EditorSessionTool.cs +++ b/Tools/EditorSessionTool.cs @@ -7,51 +7,51 @@ namespace SandboxModelContextProtocol.Server.Tools; [McpServerToolType] -public class EditorSessionTool( IEditorToolService editorToolService ) +public class EditorSessionTool( IToolService toolService ) { - private readonly IEditorToolService _editorToolService = editorToolService; + private readonly IToolService _toolService = toolService; [McpServerTool, Description( "Gets the active editor session." )] - public async Task GetActiveEditorSession() + public async Task GetActiveEditorSession() { - var command = new CallEditorToolRequest() + var command = new CallToolRequest() { Name = nameof( GetActiveEditorSession ), }; - return await _editorToolService.CallTool( command ); + return await _toolService.CallTool( command ); } [McpServerTool, Description( "Gets all editor sessions." )] - public async Task GetAllEditorSessions() + public async Task GetAllEditorSessions() { - var command = new CallEditorToolRequest() + var command = new CallToolRequest() { Name = nameof( GetAllEditorSessions ), }; - return await _editorToolService.CallTool( command ); + return await _toolService.CallTool( command ); } [McpServerTool, Description( "Saves all editor sessions." )] - public async Task SaveAllEditorSessions() + public async Task SaveAllEditorSessions() { - var command = new CallEditorToolRequest() + var command = new CallToolRequest() { Name = nameof( SaveAllEditorSessions ), }; - return await _editorToolService.CallTool( command ); + return await _toolService.CallTool( command ); } [McpServerTool, Description( "Saves the active editor session." )] - public async Task SaveActiveEditorSession() + public async Task SaveActiveEditorSession() { - var command = new CallEditorToolRequest() + var command = new CallToolRequest() { Name = nameof( SaveActiveEditorSession ), }; - return await _editorToolService.CallTool( command ); + return await _toolService.CallTool( command ); } -} \ No newline at end of file +} diff --git a/Tools/GameObjectTool.cs b/Tools/GameObjectTool.cs index e9574ba..8edc3a4 100644 --- a/Tools/GameObjectTool.cs +++ b/Tools/GameObjectTool.cs @@ -9,14 +9,14 @@ namespace SandboxModelContextProtocol.Server.Tools; [McpServerToolType] -public class GameObjectTool( IEditorToolService editorToolService ) +public class GameObjectTool( IToolService toolService ) { - private readonly IEditorToolService _editorToolService = editorToolService; + private readonly IToolService _toolService = toolService; [McpServerTool, Description( "Gets a game object by name." )] - public async Task GetGameObjectByName( string name, string? sceneId = null ) + public async Task GetGameObjectByName( string name, string? sceneId = null ) { - var command = new CallEditorToolRequest() + var command = new CallToolRequest() { Name = nameof( GetGameObjectByName ), Arguments = new Dictionary() @@ -26,13 +26,13 @@ public async Task GetGameObjectByName( string name, stri }, }; - return await _editorToolService.CallTool( command ); + return await _toolService.CallTool( command ); } [McpServerTool, Description( "Gets a game object by id." )] - public async Task GetGameObjectById( string id, string? sceneId = null ) + public async Task GetGameObjectById( string id, string? sceneId = null ) { - var command = new CallEditorToolRequest() + var command = new CallToolRequest() { Name = nameof( GetGameObjectById ), Arguments = new Dictionary() @@ -42,13 +42,13 @@ public async Task GetGameObjectById( string id, string? }, }; - return await _editorToolService.CallTool( command ); + return await _toolService.CallTool( command ); } [McpServerTool, Description( "Gets all game objects." )] - public async Task GetAllGameObjects( string? sceneId = null ) + public async Task GetAllGameObjects( string? sceneId = null ) { - var command = new CallEditorToolRequest() + var command = new CallToolRequest() { Name = nameof( GetAllGameObjects ), Arguments = new Dictionary() @@ -57,13 +57,13 @@ public async Task GetAllGameObjects( string? sceneId = n }, }; - return await _editorToolService.CallTool( command ); + return await _toolService.CallTool( command ); } [McpServerTool, Description( "Creates a new game object." )] - public async Task CreateGameObject( string name, string sceneId, string? parentId = null ) + public async Task CreateGameObject( string name, string sceneId, string? parentId = null ) { - var command = new CallEditorToolRequest() + var command = new CallToolRequest() { Name = nameof( CreateGameObject ), Arguments = new Dictionary() @@ -74,13 +74,13 @@ public async Task CreateGameObject( string name, string }, }; - return await _editorToolService.CallTool( command ); + return await _toolService.CallTool( command ); } [McpServerTool, Description( "Duplicates a game object." )] - public async Task DuplicateGameObject( string id, string? sceneId = null, string? parentId = null ) + public async Task DuplicateGameObject( string id, string? sceneId = null, string? parentId = null ) { - var command = new CallEditorToolRequest() + var command = new CallToolRequest() { Name = nameof( DuplicateGameObject ), Arguments = new Dictionary() @@ -91,13 +91,13 @@ public async Task DuplicateGameObject( string id, string }, }; - return await _editorToolService.CallTool( command ); + return await _toolService.CallTool( command ); } [McpServerTool, Description( "Destroys (deletes) a game object." )] - public async Task DestroyGameObject( string id, string? sceneId = null ) + public async Task DestroyGameObject( string id, string? sceneId = null ) { - var command = new CallEditorToolRequest() + var command = new CallToolRequest() { Name = nameof( DestroyGameObject ), Arguments = new Dictionary() @@ -107,14 +107,14 @@ public async Task DestroyGameObject( string id, string? }, }; - return await _editorToolService.CallTool( command ); + return await _toolService.CallTool( command ); } // Transform Commands [McpServerTool, Description( "Sets the world position of a game object." )] - public async Task SetGameObjectWorldPosition( string id, float x, float y, float z, string? sceneId = null ) + public async Task SetGameObjectWorldPosition( string id, float x, float y, float z, string? sceneId = null ) { - var command = new CallEditorToolRequest() + var command = new CallToolRequest() { Name = nameof( SetGameObjectWorldPosition ), Arguments = new Dictionary() @@ -127,13 +127,13 @@ public async Task SetGameObjectWorldPosition( string id, }, }; - return await _editorToolService.CallTool( command ); + return await _toolService.CallTool( command ); } [McpServerTool, Description( "Sets the world rotation of a game object." )] - public async Task SetGameObjectWorldRotation( string id, float x, float y, float z, float w, string? sceneId = null ) + public async Task SetGameObjectWorldRotation( string id, float x, float y, float z, float w, string? sceneId = null ) { - var command = new CallEditorToolRequest() + var command = new CallToolRequest() { Name = nameof( SetGameObjectWorldRotation ), Arguments = new Dictionary() @@ -147,13 +147,13 @@ public async Task SetGameObjectWorldRotation( string id, }, }; - return await _editorToolService.CallTool( command ); + return await _toolService.CallTool( command ); } [McpServerTool, Description( "Sets the world scale of a game object." )] - public async Task SetGameObjectWorldScale( string id, float x, float y, float z, string? sceneId = null ) + public async Task SetGameObjectWorldScale( string id, float x, float y, float z, string? sceneId = null ) { - var command = new CallEditorToolRequest() + var command = new CallToolRequest() { Name = nameof( SetGameObjectWorldScale ), Arguments = new Dictionary() @@ -166,13 +166,13 @@ public async Task SetGameObjectWorldScale( string id, fl }, }; - return await _editorToolService.CallTool( command ); + return await _toolService.CallTool( command ); } [McpServerTool, Description( "Sets the local position of a game object." )] - public async Task SetGameObjectLocalPosition( string id, float x, float y, float z, string? sceneId = null ) + public async Task SetGameObjectLocalPosition( string id, float x, float y, float z, string? sceneId = null ) { - var command = new CallEditorToolRequest() + var command = new CallToolRequest() { Name = nameof( SetGameObjectLocalPosition ), Arguments = new Dictionary() @@ -185,13 +185,13 @@ public async Task SetGameObjectLocalPosition( string id, }, }; - return await _editorToolService.CallTool( command ); + return await _toolService.CallTool( command ); } [McpServerTool, Description( "Sets the local rotation of a game object." )] - public async Task SetGameObjectLocalRotation( string id, float x, float y, float z, float w, string? sceneId = null ) + public async Task SetGameObjectLocalRotation( string id, float x, float y, float z, float w, string? sceneId = null ) { - var command = new CallEditorToolRequest() + var command = new CallToolRequest() { Name = nameof( SetGameObjectLocalRotation ), Arguments = new Dictionary() @@ -205,13 +205,13 @@ public async Task SetGameObjectLocalRotation( string id, }, }; - return await _editorToolService.CallTool( command ); + return await _toolService.CallTool( command ); } [McpServerTool, Description( "Sets the local scale of a game object." )] - public async Task SetGameObjectLocalScale( string id, float x, float y, float z, string? sceneId = null ) + public async Task SetGameObjectLocalScale( string id, float x, float y, float z, string? sceneId = null ) { - var command = new CallEditorToolRequest() + var command = new CallToolRequest() { Name = nameof( SetGameObjectLocalScale ), Arguments = new Dictionary() @@ -224,14 +224,14 @@ public async Task SetGameObjectLocalScale( string id, fl }, }; - return await _editorToolService.CallTool( command ); + return await _toolService.CallTool( command ); } // Hierarchy Commands [McpServerTool, Description( "Sets the parent of a game object." )] - public async Task SetGameObjectParent( string id, string? parentId, string? sceneId = null ) + public async Task SetGameObjectParent( string id, string? parentId, string? sceneId = null ) { - var command = new CallEditorToolRequest() + var command = new CallToolRequest() { Name = nameof( SetGameObjectParent ), Arguments = new Dictionary() @@ -242,13 +242,13 @@ public async Task SetGameObjectParent( string id, string }, }; - return await _editorToolService.CallTool( command ); + return await _toolService.CallTool( command ); } [McpServerTool, Description( "Gets all child game objects of a game object." )] - public async Task GetGameObjectChildren( string id, string? sceneId = null ) + public async Task GetGameObjectChildren( string id, string? sceneId = null ) { - var command = new CallEditorToolRequest() + var command = new CallToolRequest() { Name = nameof( GetGameObjectChildren ), Arguments = new Dictionary() @@ -258,13 +258,13 @@ public async Task GetGameObjectChildren( string id, stri }, }; - return await _editorToolService.CallTool( command ); + return await _toolService.CallTool( command ); } [McpServerTool, Description( "Gets the parent of a game object." )] - public async Task GetGameObjectParent( string id, string? sceneId = null ) + public async Task GetGameObjectParent( string id, string? sceneId = null ) { - var command = new CallEditorToolRequest() + var command = new CallToolRequest() { Name = nameof( GetGameObjectParent ), Arguments = new Dictionary() @@ -274,14 +274,14 @@ public async Task GetGameObjectParent( string id, string }, }; - return await _editorToolService.CallTool( command ); + return await _toolService.CallTool( command ); } // Property Commands [McpServerTool, Description( "Sets the name of a game object." )] - public async Task SetGameObjectName( string id, string name, string? sceneId = null ) + public async Task SetGameObjectName( string id, string name, string? sceneId = null ) { - var command = new CallEditorToolRequest() + var command = new CallToolRequest() { Name = nameof( SetGameObjectName ), Arguments = new Dictionary() @@ -292,13 +292,13 @@ public async Task SetGameObjectName( string id, string n }, }; - return await _editorToolService.CallTool( command ); + return await _toolService.CallTool( command ); } [McpServerTool, Description( "Sets the enabled state of a game object." )] - public async Task SetGameObjectEnabled( string id, bool enabled, string? sceneId = null ) + public async Task SetGameObjectEnabled( string id, bool enabled, string? sceneId = null ) { - var command = new CallEditorToolRequest() + var command = new CallToolRequest() { Name = nameof( SetGameObjectEnabled ), Arguments = new Dictionary() @@ -309,14 +309,14 @@ public async Task SetGameObjectEnabled( string id, bool }, }; - return await _editorToolService.CallTool( command ); + return await _toolService.CallTool( command ); } // Component Commands [McpServerTool, Description( "Adds a component to a game object." )] - public async Task AddGameObjectComponent( string id, string componentType, string? sceneId = null ) + public async Task AddGameObjectComponent( string id, string componentType, string? sceneId = null ) { - var command = new CallEditorToolRequest() + var command = new CallToolRequest() { Name = nameof( AddGameObjectComponent ), Arguments = new Dictionary() @@ -327,13 +327,13 @@ public async Task AddGameObjectComponent( string id, str }, }; - return await _editorToolService.CallTool( command ); + return await _toolService.CallTool( command ); } [McpServerTool, Description( "Removes a component from a game object." )] - public async Task RemoveGameObjectComponent( string id, string componentType, string? sceneId = null ) + public async Task RemoveGameObjectComponent( string id, string componentType, string? sceneId = null ) { - var command = new CallEditorToolRequest() + var command = new CallToolRequest() { Name = nameof( RemoveGameObjectComponent ), Arguments = new Dictionary() @@ -344,13 +344,13 @@ public async Task RemoveGameObjectComponent( string id, }, }; - return await _editorToolService.CallTool( command ); + return await _toolService.CallTool( command ); } [McpServerTool, Description( "Gets all components of a game object." )] - public async Task GetGameObjectComponents( string id, string? sceneId = null ) + public async Task GetGameObjectComponents( string id, string? sceneId = null ) { - var command = new CallEditorToolRequest() + var command = new CallToolRequest() { Name = nameof( GetGameObjectComponents ), Arguments = new Dictionary() @@ -360,13 +360,13 @@ public async Task GetGameObjectComponents( string id, st }, }; - return await _editorToolService.CallTool( command ); + return await _toolService.CallTool( command ); } [McpServerTool, Description( "Gets a specific component of a game object." )] - public async Task GetGameObjectComponent( string id, string componentType, string? sceneId = null ) + public async Task GetGameObjectComponent( string id, string componentType, string? sceneId = null ) { - var command = new CallEditorToolRequest() + var command = new CallToolRequest() { Name = nameof( GetGameObjectComponent ), Arguments = new Dictionary() @@ -377,13 +377,13 @@ public async Task GetGameObjectComponent( string id, str }, }; - return await _editorToolService.CallTool( command ); + return await _toolService.CallTool( command ); } [McpServerTool, Description( "Sets properties of a component on a game object." )] - public async Task SetGameObjectComponentProperty( string id, string componentType, string propertyName, JsonElement value, string? sceneId = null ) + public async Task SetGameObjectComponentProperty( string id, string componentType, string propertyName, JsonElement value, string? sceneId = null ) { - var command = new CallEditorToolRequest() + var command = new CallToolRequest() { Name = nameof( SetGameObjectComponentProperty ), Arguments = new Dictionary() @@ -396,6 +396,6 @@ public async Task SetGameObjectComponentProperty( string }, }; - return await _editorToolService.CallTool( command ); + return await _toolService.CallTool( command ); } } diff --git a/appsettings.json b/appsettings.json deleted file mode 100644 index 92e8235..0000000 --- a/appsettings.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "WebSocket": { - "Url": "http://localhost:8080", - "Path": "/ws" - } -} \ No newline at end of file From 6735d9f71e82322c4b52f84e8351e91f8bb9e408 Mon Sep 17 00:00:00 2001 From: suiramdev Date: Sun, 3 Aug 2025 18:32:11 +0200 Subject: [PATCH 2/8] feat: add Docker support with configuration files and health check endpoint --- .dockerignore | 54 +++++++++++ .github/workflows/docker-deploy.yml | 113 +++++++++++++++++++++++ .github/workflows/docker.yml | 56 ++++++++++++ Dockerfile | 47 ++++++++++ Program.cs | 5 ++ README.md | 133 +++++++++++----------------- appsettings.json | 17 ++++ build.ps1 | 124 -------------------------- 8 files changed, 345 insertions(+), 204 deletions(-) create mode 100644 .dockerignore create mode 100644 .github/workflows/docker-deploy.yml create mode 100644 .github/workflows/docker.yml create mode 100644 Dockerfile create mode 100644 appsettings.json delete mode 100644 build.ps1 diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..78163a2 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,54 @@ +# Git +.git +.gitignore +.gitattributes + +# Docker +Dockerfile* +docker-compose* +.dockerignore + +# Documentation +README.md +*.md +Assets/ + +# Build output +bin/ +obj/ +nupkg/ + +# IDE and Editor files +.vs/ +.vscode/ +*.swp +*.swo +*~ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Logs +*.log + +# Runtime +*.dll.config +*.exe.config + +# NuGet +*.nupkg +*.snupkg + +# User specific files +*.user +*.suo + +# Temporary files +*.tmp +*.temp \ No newline at end of file diff --git a/.github/workflows/docker-deploy.yml b/.github/workflows/docker-deploy.yml new file mode 100644 index 0000000..20aae91 --- /dev/null +++ b/.github/workflows/docker-deploy.yml @@ -0,0 +1,113 @@ +name: Docker Build and Deploy + +on: + push: + branches: [ main, master ] + tags: [ 'v*' ] + pull_request: + branches: [ main, master ] + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build-and-push: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Container Registry + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=sha,prefix={{branch}}- + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + platforms: linux/amd64,linux/arm64 + + test: + runs-on: ubuntu-latest + needs: build-and-push + if: github.event_name == 'pull_request' + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Build Docker image for testing + run: docker build -t sbox-mcp-server-test . + + - name: Test Docker image + run: | + # Start the container + docker run -d --name sbox-mcp-server-test -p 8080:8080 sbox-mcp-server-test + + # Wait for the container to be ready + sleep 10 + + # Check if container is running + docker ps | grep sbox-mcp-server-test + + # Check container logs + docker logs sbox-mcp-server-test + + # Test health check (if available) + curl -f http://localhost:8080/health || echo "Health check endpoint not available" + + # Clean up + docker stop sbox-mcp-server-test + docker rm sbox-mcp-server-test + + security-scan: + runs-on: ubuntu-latest + needs: build-and-push + if: github.event_name != 'pull_request' + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@master + with: + image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} + format: 'sarif' + output: 'trivy-results.sarif' + + - name: Upload Trivy scan results to GitHub Security tab + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: 'trivy-results.sarif' \ No newline at end of file diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..a0fb101 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,56 @@ +name: Docker Build and Push + +on: + push: + branches: [ main, master ] + tags: [ 'v*' ] + pull_request: + branches: [ main, master ] + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=sha,prefix={{branch}}- + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e226be2 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,47 @@ +# Use the official .NET 9.0 SDK image for building +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build +WORKDIR /src + +# Copy project file and restore dependencies (better layer caching) +COPY ["modelcontextprotocol.csproj", "./"] +RUN dotnet restore "modelcontextprotocol.csproj" --runtime linux-x64 + +# Copy source code and build the application +COPY . . +RUN dotnet publish "modelcontextprotocol.csproj" -c Release -o /app/publish \ + --no-restore \ + --self-contained true \ + --runtime linux-x64 \ + /p:PublishTrimmed=false + +# Use a minimal base image for self-contained deployment +FROM mcr.microsoft.com/dotnet/runtime-deps:9.0 AS runtime + +# Install curl for health checks +RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Create a non-root user for security +RUN adduser --disabled-password --gecos '' --uid 1001 appuser && \ + chown -R appuser:appuser /app +USER appuser + +# Copy the published application from the build stage +COPY --from=build /app/publish . + +# Expose the port the app runs on +EXPOSE 8080 + +# Set environment variables for ASP.NET Core +ENV ASPNETCORE_URLS=http://0.0.0.0:8080 \ + ASPNETCORE_ENVIRONMENT=Production \ + DOTNET_RUNNING_IN_CONTAINER=true \ + DOTNET_USE_POLLING_FILE_WATCHER=true + +# Add health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \ + CMD curl -f http://localhost:8080/health || exit 1 + +# Run the self-contained application +ENTRYPOINT ["./SandboxModelContextProtocol.Server"] \ No newline at end of file diff --git a/Program.cs b/Program.cs index 92e6747..6957929 100644 --- a/Program.cs +++ b/Program.cs @@ -1,9 +1,11 @@ +using System; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; using SandboxModelContextProtocol.Server.Services; using SandboxModelContextProtocol.Server.Services.Interfaces; using SandboxModelContextProtocol.Server.Services.Models; @@ -52,6 +54,9 @@ public static async Task Main( string[] args ) // Map MCP endpoints for HTTP transport app.MapMcp(); + // Add health check endpoint for Docker + app.MapGet( "/health", () => Results.Ok( new { status = "healthy", timestamp = DateTime.UtcNow } ) ); + // Configure WebSocket endpoint var webSocketService = app.Services.GetRequiredService(); diff --git a/README.md b/README.md index b502393..499ef4a 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ [![.NET](https://img.shields.io/badge/.NET-9.0-blue)](https://dotnet.microsoft.com/) [![s&box](https://img.shields.io/badge/s%26box-Compatible-orange)](https://sbox.game/) +[![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/install-mcp?name=sbox&config=JTdCJTIyY29tbWFuZCUyMiUzQSUyMmRvY2tlciUyMHJ1biUyMC0tcm0lMjAtaSUyMC0tbmFtZSUyMHNib3gtbWNwLXNlcnZlci1jdXJzb3IlMjAtLWZvcmNlLXJtJTIwLXAlMjA4MDgwJTNBODA4MCUyMGdoY3IuaW8lMkZzdWlyYW1kZXYlMkZzYm94LW1jcC1zZXJ2ZXIlM0FtYWluJTIyJTJDJTIydHlwZSUyMiUzQSUyMnN0ZGlvJTIyJTJDJTIyZW5hYmxlJTIyJTNBdHJ1ZSUyQyUyMmVudiUyMiUzQSU3QiUyMkRPQ0tFUl9CVUlMREtJVCUyMiUzQSUyMjElMjIlN0QlN0Q%3D) + > [!IMPORTANT] > This project is currently under active development. @@ -14,73 +16,92 @@ This MCP server works in conjunction with the separate [s&box Adapter Library](h ## Prerequisites -- [.NET 9.0 SDK](https://dotnet.microsoft.com/download/dotnet/9.0) or later -- [s&box](https://sbox.game/) (latest version) -- [s&box Adapter Library](https://github.com/suiramdev/sbox-mcp-library) (must be installed separately) -- An MCP-compatible AI assistant (Claude Desktop, Cursor, etc.) +- [Docker](https://www.docker.com/get-started) (recommended) +- [s&box](https://sbox.game/) +- An [AI assistant that supports MCP](https://docs.cursor.com/mcp/introduction) (for example, Cursor, Claude Desktop, etc.) is suggested ## Quick Start ### Step 1: Install and Run the MCP Server -1. **Clone this repository** +To get started, make sure you have Docker installed on your system. Cursor users can simply click the button below to install and launch the MCP Server automatically, or you can follow the manual instructions below. - ```bash - git clone https://github.com/suiramdev/sbox-mcp-server.git - cd sbox-mcp-server - ``` +[![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/install-mcp?name=sbox&config=JTdCJTIyY29tbWFuZCUyMiUzQSUyMmRvY2tlciUyMHJ1biUyMC0tcm0lMjAtaSUyMC0tbmFtZSUyMHNib3gtbWNwLXNlcnZlci1jdXJzb3IlMjAtLWZvcmNlLXJtJTIwLXAlMjA4MDgwJTNBODA4MCUyMGdoY3IuaW8lMkZzdWlyYW1kZXYlMkZzYm94LW1jcC1zZXJ2ZXIlM0FtYWluJTIyJTJDJTIydHlwZSUyMiUzQSUyMnN0ZGlvJTIyJTJDJTIyZW5hYmxlJTIyJTNBdHJ1ZSUyQyUyMmVudiUyMiUzQSU3QiUyMkRPQ0tFUl9CVUlMREtJVCUyMiUzQSUyMjElMjIlN0QlN0Q%3D) -2. **Build the server** +
+Manual Installation using Docker - **Using the Build Script (Recommended):** +
- ```powershell - .\build.ps1 +1. **Build the Docker image** + ```bash + docker build -t sbox-mcp-server . ``` - - **Manual Build:** - +2. **Run the container** ```bash - dotnet build + docker run -d -p 8080:8080 --name sbox-mcp-server sbox-mcp-server + ``` +3. **Use the MCP Server in your AI assistant** + ```json + { + "mcpServers": { + "sbox": { + "command": "docker", + "args": [ + "run", + "--rm", + "-i", + "--name", "sbox-mcp-server-cursor", + "--force-rm", + "-p", "8080:8080", + "sbox-mcp-server" + ] + } + } + } ``` -3. **Configure your AI assistant** +
- Add the MCP server to your AI assistant configuration: +
+Manual Installation using .NET SDK - **For Cursor Editor (mcp.json):** +
+1. **Build the server** + ```bash + dotnet build + ``` +2. **Run the container** + ```bash + dotnet run + ``` +3. **Use the MCP Server in your AI assistant** ```json { "mcpServers": { "sbox": { - "command": "cmd", - "type": "stdio", - "enable": true, - "args": [ - "/c", - "\\bin\\win-x64\\SandboxModelContextProtocol.Server.exe" - ] + "transport": "http", + "url": "http://localhost:8080" } } } ``` -> [!IMPORTANT] -> The server must be running for the s&box adapter library to function. Please ensure the server is running before proceeding to the next step. +
### Step 2: Install the Adapter Library in s&box -Before you can interact with s&box, you must install the adapter library: +To enable interaction with s&box, you need to install the adapter library. This library allows the MCP Server to communicate with the s&box editor. 1. **Install the Adapter Library** from the [sbox-mcp-library repository](https://github.com/suiramdev/sbox-mcp-library) + 2. **Follow the setup instructions** in the Adapter Library repository to: - Install the library in your s&box project through the Asset Library - Connect to this MCP Server -3. **Ensure both components are connected** before using AI assistant commands > [!IMPORTANT] -> This MCP Server requires the separate [s&box Adapter Library](https://github.com/suiramdev/sbox-mcp-library) to communicate with the s&box editor. Please refer to the [Adapter Library documentation](https://github.com/suiramdev/sbox-mcp-library) for detailed installation and usage instructions. +> The server must be running for the s&box adapter library to function. Please ensure the server is running before proceeding to the next step. ## Usage @@ -94,22 +115,6 @@ Once both this MCP Server and the s&box Adapter Library are installed and connec "Show me all components attached to the Ground object" ``` -## Troubleshooting - -### Server Connection Issues - -If the MCP Server fails to start: - -1. **Verify .NET 9.0 SDK is installed** and accessible via command line -2. **Check the console output** for error messages -3. **Ensure port 8080 is available** (or configure a different port in appsettings.json) - -### Testing the Server - -You can manually test if the MCP Server is running by: -- Using [Postman](https://www.postman.com/downloads/) or similar API clients to send WebSocket requests to `ws://localhost:8080/ws` -- Checking the server console output for connection attempts - ## Architecture This MCP Server acts as a bridge between: @@ -121,38 +126,6 @@ The MCP Server: - Sends commands to the s&box Adapter Library for execution - Returns responses back to AI assistants -## Configuration - -The server can be configured via `appsettings.json`: - -```json -{ - "WebSocket": { - "Url": "http://localhost:8080", - "Path": "/ws" - } -} -``` - -> **Note**: Port 8080 is the recommended WebSocket port for s&box local development. - -## Build Script Options - -The included PowerShell build script (`build.ps1`) provides comprehensive build management: - -| Command | Description | -| ------------------------- | ------------------------------------- | -| `.\build.ps1` | Default build (Release configuration) | -| `.\build.ps1 build` | Build in Release mode | -| `.\build.ps1 build-debug` | Build in Debug mode | -| `.\build.ps1 run` | Run the server | -| `.\build.ps1 run-debug` | Run in debug mode | -| `.\build.ps1 publish` | Create self-contained executable | -| `.\build.ps1 clean` | Clean build artifacts | -| `.\build.ps1 rebuild` | Full clean rebuild | -| `.\build.ps1 test` | Run unit tests | -| `.\build.ps1 help` | Show all available options | - ## Contributing Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change. diff --git a/appsettings.json b/appsettings.json new file mode 100644 index 0000000..60d7fe8 --- /dev/null +++ b/appsettings.json @@ -0,0 +1,17 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + }, + "Console": { + "IncludeScopes": false, + "TimestampFormat": "HH:mm:ss " + } + }, + "WebSocket": { + "Url": "http://+:8080", + "Path": "/ws" + }, + "AllowedHosts": "*" +} \ No newline at end of file diff --git a/build.ps1 b/build.ps1 deleted file mode 100644 index c63b984..0000000 --- a/build.ps1 +++ /dev/null @@ -1,124 +0,0 @@ -# PowerShell Build Script for ModelContextProtocol Server -param( - [Parameter(Position=0)] - [string]$Target = "build", - [string]$Configuration = "Release", - [string]$Runtime = "win-x64" -) - -# Project configuration -$ProjectFile = "modelcontextprotocol.csproj" -$OutputDir = "bin" - -function Show-Help { - Write-Host "Available targets:" -ForegroundColor Green - Write-Host " build - Build the project in Release configuration" - Write-Host " build-debug - Build the project in Debug configuration" - Write-Host " restore - Restore NuGet packages" - Write-Host " clean - Clean build artifacts" - Write-Host " publish - Publish the application as self-contained executable" - Write-Host " run - Run the application" - Write-Host " run-debug - Run the application in debug mode" - Write-Host " test - Run tests" - Write-Host " rebuild - Clean, restore, and build" - Write-Host " help - Show this help message" - Write-Host "" - Write-Host "Usage examples:" -ForegroundColor Yellow - Write-Host " .\build.ps1" - Write-Host " .\build.ps1 build" - Write-Host " .\build.ps1 run" - Write-Host " .\build.ps1 publish" -} - -function Test-DotNet { - try { - $version = dotnet --version - Write-Host "Using .NET SDK version: $version" -ForegroundColor Green - return $true - } - catch { - Write-Error "Error: .NET SDK is not installed or not in PATH" - return $false - } -} - -function Invoke-Build { - Write-Host "Building ModelContextProtocol Server..." -ForegroundColor Cyan - dotnet build $ProjectFile --configuration $Configuration -} - -function Invoke-BuildDebug { - Write-Host "Building ModelContextProtocol Server (Debug)..." -ForegroundColor Cyan - dotnet build $ProjectFile --configuration Debug -} - -function Invoke-Restore { - Write-Host "Restoring NuGet packages..." -ForegroundColor Cyan - dotnet restore $ProjectFile -} - -function Invoke-Clean { - Write-Host "Cleaning build artifacts..." -ForegroundColor Cyan - dotnet clean $ProjectFile - - if (Test-Path $OutputDir) { - Remove-Item $OutputDir -Recurse -Force - Write-Host "Removed $OutputDir" -ForegroundColor Yellow - } - - $objDir = "obj" - if (Test-Path $objDir) { - Remove-Item $objDir -Recurse -Force - Write-Host "Removed $objDir" -ForegroundColor Yellow - } -} - -function Invoke-Publish { - Write-Host "Publishing ModelContextProtocol Server..." -ForegroundColor Cyan - dotnet publish $ProjectFile --configuration $Configuration --runtime $Runtime --self-contained true --single-file -} - -function Invoke-Run { - Write-Host "Running ModelContextProtocol Server..." -ForegroundColor Cyan - dotnet run --project $ProjectFile -} - -function Invoke-RunDebug { - Write-Host "Running ModelContextProtocol Server (Debug)..." -ForegroundColor Cyan - dotnet run --project $ProjectFile --configuration Debug -} - -function Invoke-Test { - Write-Host "Running tests..." -ForegroundColor Cyan - dotnet test $ProjectFile -} - -function Invoke-Rebuild { - Write-Host "Performing full rebuild..." -ForegroundColor Cyan - Invoke-Clean - Invoke-Restore - Invoke-Build -} - -# Main execution -if (-not (Test-DotNet)) { - exit 1 -} - -switch ($Target.ToLower()) { - "build" { Invoke-Build } - "build-debug" { Invoke-BuildDebug } - "restore" { Invoke-Restore } - "clean" { Invoke-Clean } - "publish" { Invoke-Publish } - "run" { Invoke-Run } - "run-debug" { Invoke-RunDebug } - "test" { Invoke-Test } - "rebuild" { Invoke-Rebuild } - "help" { Show-Help } - default { - Write-Host "Unknown target: $Target" -ForegroundColor Red - Show-Help - exit 1 - } -} \ No newline at end of file From 09778311f0a5632850c85815d14e70f0f9740dd7 Mon Sep 17 00:00:00 2001 From: suiramdev Date: Sun, 3 Aug 2025 18:59:30 +0200 Subject: [PATCH 3/8] refactor: replace Docker workflows with a unified build and publish configuration --- .github/workflows/docker-deploy.yml | 113 --------------------------- .github/workflows/docker-publish.yml | 83 ++++++++++++++++++++ .github/workflows/docker.yml | 56 ------------- 3 files changed, 83 insertions(+), 169 deletions(-) delete mode 100644 .github/workflows/docker-deploy.yml create mode 100644 .github/workflows/docker-publish.yml delete mode 100644 .github/workflows/docker.yml diff --git a/.github/workflows/docker-deploy.yml b/.github/workflows/docker-deploy.yml deleted file mode 100644 index 20aae91..0000000 --- a/.github/workflows/docker-deploy.yml +++ /dev/null @@ -1,113 +0,0 @@ -name: Docker Build and Deploy - -on: - push: - branches: [ main, master ] - tags: [ 'v*' ] - pull_request: - branches: [ main, master ] - -env: - REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository }} - -jobs: - build-and-push: - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to Container Registry - if: github.event_name != 'pull_request' - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - tags: | - type=ref,event=branch - type=ref,event=pr - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=sha,prefix={{branch}}- - - - name: Build and push Docker image - uses: docker/build-push-action@v5 - with: - context: . - file: ./Dockerfile - push: ${{ github.event_name != 'pull_request' }} - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max - platforms: linux/amd64,linux/arm64 - - test: - runs-on: ubuntu-latest - needs: build-and-push - if: github.event_name == 'pull_request' - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Build Docker image for testing - run: docker build -t sbox-mcp-server-test . - - - name: Test Docker image - run: | - # Start the container - docker run -d --name sbox-mcp-server-test -p 8080:8080 sbox-mcp-server-test - - # Wait for the container to be ready - sleep 10 - - # Check if container is running - docker ps | grep sbox-mcp-server-test - - # Check container logs - docker logs sbox-mcp-server-test - - # Test health check (if available) - curl -f http://localhost:8080/health || echo "Health check endpoint not available" - - # Clean up - docker stop sbox-mcp-server-test - docker rm sbox-mcp-server-test - - security-scan: - runs-on: ubuntu-latest - needs: build-and-push - if: github.event_name != 'pull_request' - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Run Trivy vulnerability scanner - uses: aquasecurity/trivy-action@master - with: - image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} - format: 'sarif' - output: 'trivy-results.sarif' - - - name: Upload Trivy scan results to GitHub Security tab - uses: github/codeql-action/upload-sarif@v3 - if: always() - with: - sarif_file: 'trivy-results.sarif' \ No newline at end of file diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 0000000..aaba9b7 --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,83 @@ +name: Build and Publish Docker Image + +on: + push: + branches: + - main + - dev + tags: + - 'v*' + pull_request: + branches: + - main + - dev + workflow_dispatch: + inputs: + publish_branch: + description: 'Publish branch-specific image' + required: false + default: false + type: boolean + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build-and-publish: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + # Branch-based tags + type=ref,event=branch + # Tag-based versioning + type=ref,event=tag + # Semver patterns for tags + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + # Latest tag for main branch + type=raw,value=latest,enable={{is_default_branch}} + # Dev tag for dev branch + type=raw,value=dev,enable=${{ github.ref == 'refs/heads/dev' }} + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: ${{ github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/v') || github.event.inputs.publish_branch == 'true') }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Generate artifact attestation + uses: actions/attest-build-provenance@v1 + if: ${{ github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/v') || github.event.inputs.publish_branch == 'true') }} + with: + subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}} + subject-digest: ${{ steps.build.outputs.digest }} + push-to-registry: true \ No newline at end of file diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml deleted file mode 100644 index a0fb101..0000000 --- a/.github/workflows/docker.yml +++ /dev/null @@ -1,56 +0,0 @@ -name: Docker Build and Push - -on: - push: - branches: [ main, master ] - tags: [ 'v*' ] - pull_request: - branches: [ main, master ] - -env: - REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository }} - -jobs: - build: - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to GitHub Container Registry - if: github.event_name != 'pull_request' - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - tags: | - type=ref,event=branch - type=ref,event=pr - type=semver,pattern={{version}} - type=sha,prefix={{branch}}- - - - name: Build and push Docker image - uses: docker/build-push-action@v5 - with: - context: . - file: ./Dockerfile - push: ${{ github.event_name != 'pull_request' }} - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max \ No newline at end of file From 700ad579414525cd2beed52773f949618acc4d5e Mon Sep 17 00:00:00 2001 From: suiramdev Date: Sun, 3 Aug 2025 19:00:48 +0200 Subject: [PATCH 4/8] feat: add project file for Model Context Protocol server with initial configuration and dependencies --- .gitignore | 1 - modelcontextprotocol.csproj | 68 +++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 modelcontextprotocol.csproj diff --git a/.gitignore b/.gitignore index 89e277d..e8ea849 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,6 @@ # Auto-generated code editor files .vs/* .vscode/* -*.csproj obj bin Properties/* diff --git a/modelcontextprotocol.csproj b/modelcontextprotocol.csproj new file mode 100644 index 0000000..d56ca62 --- /dev/null +++ b/modelcontextprotocol.csproj @@ -0,0 +1,68 @@ + + + + Exe + net9.0 + SandboxModelContextProtocol.Server + 13 + 1701;1702;1591; + + False + false + DEBUG + False + enable + + + true + sbox-mcp-server + McpServer + false + + + true + true + win-x64 + + + + + SBox.MCP.Server + 1.0.0-preview + s&box MCP Server + suiramdev + A Model Context Protocol (MCP) server that enables AI assistants to interact with the s&box editor through real-time communication. Provides tools for managing game objects, components, and editor sessions. + https://github.com/suiramdev/sbox-mcp-server + https://github.com/suiramdev/sbox-mcp-server + git + MIT + README.md + mcp;model-context-protocol;sbox;unity;gamedev;ai;assistant;tools + Initial preview release of s&box MCP Server with game object and component management tools. + + + + bin\ + SandboxModelContextProtocol.Server + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 70b06e9d28cfb45b011ce48c4a7fdae4e8fb6bdc Mon Sep 17 00:00:00 2001 From: suiramdev Date: Sun, 3 Aug 2025 19:03:17 +0200 Subject: [PATCH 5/8] fix: update MCP Server installation button in README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 499ef4a..599575b 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![.NET](https://img.shields.io/badge/.NET-9.0-blue)](https://dotnet.microsoft.com/) [![s&box](https://img.shields.io/badge/s%26box-Compatible-orange)](https://sbox.game/) -[![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/install-mcp?name=sbox&config=JTdCJTIyY29tbWFuZCUyMiUzQSUyMmRvY2tlciUyMHJ1biUyMC0tcm0lMjAtaSUyMC0tbmFtZSUyMHNib3gtbWNwLXNlcnZlci1jdXJzb3IlMjAtLWZvcmNlLXJtJTIwLXAlMjA4MDgwJTNBODA4MCUyMGdoY3IuaW8lMkZzdWlyYW1kZXYlMkZzYm94LW1jcC1zZXJ2ZXIlM0FtYWluJTIyJTJDJTIydHlwZSUyMiUzQSUyMnN0ZGlvJTIyJTJDJTIyZW5hYmxlJTIyJTNBdHJ1ZSUyQyUyMmVudiUyMiUzQSU3QiUyMkRPQ0tFUl9CVUlMREtJVCUyMiUzQSUyMjElMjIlN0QlN0Q%3D) +[![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/install-mcp?name=sbox&config=JTdCJTIyY29tbWFuZCUyMiUzQSUyMmRvY2tlciUyMHJ1biUyMC0tcm0lMjAtaSUyMC0tbmFtZSUyMHNib3gtbWNwLXNlcnZlci1jdXJzb3IlMjAtcCUyMDgwODAlM0E4MDgwJTIwZ2hjci5pbyUyRnN1aXJhbWRldiUyRnNib3gtbWNwLXNlcnZlciUzQWxhdGVzdCUyMiUyQyUyMnR5cGUlMjIlM0ElMjJodHRwJTIyJTJDJTIyZW5hYmxlJTIyJTNBdHJ1ZSUyQyUyMnVybCUyMiUzQSUyMmh0dHAlM0ElMkYlMkZsb2NhbGhvc3QlM0E4MDgwJTIyJTdE) > [!IMPORTANT] > This project is currently under active development. @@ -26,7 +26,7 @@ This MCP server works in conjunction with the separate [s&box Adapter Library](h To get started, make sure you have Docker installed on your system. Cursor users can simply click the button below to install and launch the MCP Server automatically, or you can follow the manual instructions below. -[![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/install-mcp?name=sbox&config=JTdCJTIyY29tbWFuZCUyMiUzQSUyMmRvY2tlciUyMHJ1biUyMC0tcm0lMjAtaSUyMC0tbmFtZSUyMHNib3gtbWNwLXNlcnZlci1jdXJzb3IlMjAtLWZvcmNlLXJtJTIwLXAlMjA4MDgwJTNBODA4MCUyMGdoY3IuaW8lMkZzdWlyYW1kZXYlMkZzYm94LW1jcC1zZXJ2ZXIlM0FtYWluJTIyJTJDJTIydHlwZSUyMiUzQSUyMnN0ZGlvJTIyJTJDJTIyZW5hYmxlJTIyJTNBdHJ1ZSUyQyUyMmVudiUyMiUzQSU3QiUyMkRPQ0tFUl9CVUlMREtJVCUyMiUzQSUyMjElMjIlN0QlN0Q%3D) +[![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/install-mcp?name=sbox&config=JTdCJTIyY29tbWFuZCUyMiUzQSUyMmRvY2tlciUyMHJ1biUyMC0tcm0lMjAtaSUyMC0tbmFtZSUyMHNib3gtbWNwLXNlcnZlci1jdXJzb3IlMjAtcCUyMDgwODAlM0E4MDgwJTIwZ2hjci5pbyUyRnN1aXJhbWRldiUyRnNib3gtbWNwLXNlcnZlciUzQWxhdGVzdCUyMiUyQyUyMnR5cGUlMjIlM0ElMjJodHRwJTIyJTJDJTIyZW5hYmxlJTIyJTNBdHJ1ZSUyQyUyMnVybCUyMiUzQSUyMmh0dHAlM0ElMkYlMkZsb2NhbGhvc3QlM0E4MDgwJTIyJTdE)
Manual Installation using Docker From 25bd82723e5f6ec9c34539b6416a77789ad0003e Mon Sep 17 00:00:00 2001 From: suiramdev Date: Sun, 3 Aug 2025 19:08:37 +0200 Subject: [PATCH 6/8] chore: remove NuGet package metadata and tool configuration from project file --- modelcontextprotocol.csproj | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/modelcontextprotocol.csproj b/modelcontextprotocol.csproj index d56ca62..aad2871 100644 --- a/modelcontextprotocol.csproj +++ b/modelcontextprotocol.csproj @@ -13,33 +13,12 @@ False enable - - true - sbox-mcp-server - McpServer - false - true true win-x64 - - - SBox.MCP.Server - 1.0.0-preview - s&box MCP Server - suiramdev - A Model Context Protocol (MCP) server that enables AI assistants to interact with the s&box editor through real-time communication. Provides tools for managing game objects, components, and editor sessions. - https://github.com/suiramdev/sbox-mcp-server - https://github.com/suiramdev/sbox-mcp-server - git - MIT - README.md - mcp;model-context-protocol;sbox;unity;gamedev;ai;assistant;tools - Initial preview release of s&box MCP Server with game object and component management tools. - bin\ @@ -59,10 +38,4 @@ - - - - - - \ No newline at end of file From 33333d982e07bea6b150f9c278a6e434bef95b7f Mon Sep 17 00:00:00 2001 From: suiramdev Date: Sun, 3 Aug 2025 20:02:46 +0200 Subject: [PATCH 7/8] fix: update server URL binding to allow external access --- Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Program.cs b/Program.cs index 6957929..a5c4621 100644 --- a/Program.cs +++ b/Program.cs @@ -19,7 +19,7 @@ public static async Task Main( string[] args ) { var builder = WebApplication.CreateBuilder( args ); - builder.WebHost.UseUrls( "http://localhost:8080" ); + builder.WebHost.UseUrls( "http://0.0.0.0:8080" ); // Configure logging for HTTP transport builder.Logging.AddConsole(); From 161ef42a8b8cfbe8f80eba970562ca757bcd5cc7 Mon Sep 17 00:00:00 2001 From: suiramdev Date: Sun, 3 Aug 2025 20:08:26 +0200 Subject: [PATCH 8/8] docs: update README.md to clarify installation instructions and add .NET SDK as an alternative --- README.md | 61 ++++++++++++++----------------------------------------- 1 file changed, 15 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index 599575b..8b41caf 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,6 @@ [![.NET](https://img.shields.io/badge/.NET-9.0-blue)](https://dotnet.microsoft.com/) [![s&box](https://img.shields.io/badge/s%26box-Compatible-orange)](https://sbox.game/) -[![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/install-mcp?name=sbox&config=JTdCJTIyY29tbWFuZCUyMiUzQSUyMmRvY2tlciUyMHJ1biUyMC0tcm0lMjAtaSUyMC0tbmFtZSUyMHNib3gtbWNwLXNlcnZlci1jdXJzb3IlMjAtcCUyMDgwODAlM0E4MDgwJTIwZ2hjci5pbyUyRnN1aXJhbWRldiUyRnNib3gtbWNwLXNlcnZlciUzQWxhdGVzdCUyMiUyQyUyMnR5cGUlMjIlM0ElMjJodHRwJTIyJTJDJTIyZW5hYmxlJTIyJTNBdHJ1ZSUyQyUyMnVybCUyMiUzQSUyMmh0dHAlM0ElMkYlMkZsb2NhbGhvc3QlM0E4MDgwJTIyJTdE) - > [!IMPORTANT] > This project is currently under active development. @@ -16,7 +14,7 @@ This MCP server works in conjunction with the separate [s&box Adapter Library](h ## Prerequisites -- [Docker](https://www.docker.com/get-started) (recommended) +- [Docker](https://www.docker.com/get-started) (recommended) or [.NET SDK](https://dotnet.microsoft.com/download) (for manual installation) - [s&box](https://sbox.game/) - An [AI assistant that supports MCP](https://docs.cursor.com/mcp/introduction) (for example, Cursor, Claude Desktop, etc.) is suggested @@ -26,42 +24,17 @@ This MCP server works in conjunction with the separate [s&box Adapter Library](h To get started, make sure you have Docker installed on your system. Cursor users can simply click the button below to install and launch the MCP Server automatically, or you can follow the manual instructions below. -[![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/install-mcp?name=sbox&config=JTdCJTIyY29tbWFuZCUyMiUzQSUyMmRvY2tlciUyMHJ1biUyMC0tcm0lMjAtaSUyMC0tbmFtZSUyMHNib3gtbWNwLXNlcnZlci1jdXJzb3IlMjAtcCUyMDgwODAlM0E4MDgwJTIwZ2hjci5pbyUyRnN1aXJhbWRldiUyRnNib3gtbWNwLXNlcnZlciUzQWxhdGVzdCUyMiUyQyUyMnR5cGUlMjIlM0ElMjJodHRwJTIyJTJDJTIyZW5hYmxlJTIyJTNBdHJ1ZSUyQyUyMnVybCUyMiUzQSUyMmh0dHAlM0ElMkYlMkZsb2NhbGhvc3QlM0E4MDgwJTIyJTdE) - -
-Manual Installation using Docker -
+1. **Run the container** -1. **Build the Docker image** ```bash - docker build -t sbox-mcp-server . - ``` -2. **Run the container** - ```bash - docker run -d -p 8080:8080 --name sbox-mcp-server sbox-mcp-server - ``` -3. **Use the MCP Server in your AI assistant** - ```json - { - "mcpServers": { - "sbox": { - "command": "docker", - "args": [ - "run", - "--rm", - "-i", - "--name", "sbox-mcp-server-cursor", - "--force-rm", - "-p", "8080:8080", - "sbox-mcp-server" - ] - } - } - } + docker run -d -p 8080:8080 --name sbox-mcp-server ghcr.io/suiramdev/sbox-mcp-server:latest ``` -
+2. **Connect your AI assistant to the running MCP Server** + + [![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/install-mcp?name=sbox-mcp-server&config=JTdCJTIydHlwZSUyMiUzQSUyMmh0dHAlMjIlMkMlMjJ1cmwlMjIlM0ElMjJodHRwJTNBJTJGJTJGbG9jYWxob3N0JTNBODA4MCUyMiU3RA%3D%3D) +
Manual Installation using .NET SDK @@ -69,24 +42,20 @@ To get started, make sure you have Docker installed on your system. Cursor users
1. **Build the server** + ```bash dotnet build ``` -2. **Run the container** + +2. **Run the server** + ```bash dotnet run ``` -3. **Use the MCP Server in your AI assistant** - ```json - { - "mcpServers": { - "sbox": { - "transport": "http", - "url": "http://localhost:8080" - } - } - } - ``` + +3. **Connect your AI assistant to the running MCP Server** + + [![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/install-mcp?name=sbox-mcp-server&config=JTdCJTIydHlwZSUyMiUzQSUyMmh0dHAlMjIlMkMlMjJ1cmwlMjIlM0ElMjJodHRwJTNBJTJGJTJGbG9jYWxob3N0JTNBODA4MCUyMiU3RA%3D%3D)