Skip to content
This repository was archived by the owner on Mar 18, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -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
86 changes: 86 additions & 0 deletions .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
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
id-token: write # Required for OIDC token generation
attestations: write # Required to create attestations

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
id: build
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
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
# Auto-generated code editor files
.vs/*
.vscode/*
*.csproj
obj
bin
Properties/*
Expand Down
47 changes: 47 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
62 changes: 48 additions & 14 deletions Program.cs
Original file line number Diff line number Diff line change
@@ -1,44 +1,78 @@
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;
using Microsoft.Extensions.Configuration;

namespace SandboxModelContextProtocol.Server;

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://0.0.0.0:8080" );

// Configure logging for HTTP transport
builder.Logging.AddConsole();

// Configure WebSocket options
builder.Services.Configure<WebSocketOptions>(
builder.Services.Configure<Services.Models.WebSocketOptions>(
builder.Configuration.GetSection( "WebSocket" )
);

// Register the command service
builder.Services.AddSingleton<IEditorToolService, EditorToolService>();
builder.Services.AddSingleton<IToolService, ToolService>();

// Configure MCP Server with stdio transport and tools from assembly
// Register the resource service
builder.Services.AddSingleton<IResourceService, ResourceService>();

// 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<WebSocketService>();
builder.Services.AddSingleton<IWebSocketService>( provider => provider.GetRequiredService<WebSocketService>() );
builder.Services.AddHostedService<WebSocketService>( provider => provider.GetRequiredService<WebSocketService>() );

await builder.Build().RunAsync();
var app = builder.Build();

// Enable WebSocket support
app.UseWebSockets();

// 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<WebSocketService>();

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();
}
}
Loading
Loading