Skip to content

Commit 3c0f31f

Browse files
committed
Improve MCP server security with token-in-path authentication
- Add token-based authentication using path segments (/$token/endpoint) - Generate random 128-bit secret per Docker session - Remove sessionId parameter from ExecuteCommand (single session model) - Add --verbose flag to control HTTP logging - Add --secret flag for authentication token - Pass token via MCP_APPROVAL_SERVER_TOKEN environment variable - Update CLAUDE.md with security model documentation
1 parent 8150d76 commit 3c0f31f

File tree

8 files changed

+113
-23
lines changed

8 files changed

+113
-23
lines changed

.claude/settings.local.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@
1616
"Bash(findstr:*)",
1717
"Bash(docker run:*)",
1818
"Bash(.DockerBuild.ps1 -Claude -SkipBuild)",
19-
"Bash(dotnet restore:*)"
19+
"Bash(dotnet restore:*)",
20+
"WebFetch(domain:github.com)",
21+
"Bash(cat:*)",
22+
"Bash(npx @anthropic-ai/claude-code:*)"
2023
],
2124
"deny": [],
2225
"ask": []

CLAUDE.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,14 @@ Before any work in this repo, read these skills:
1818

1919
When running inside a Docker container, you have access to the `host-approval` MCP server for executing privileged commands on the host machine. These commands require human approval before execution.
2020

21+
### Security Model
22+
23+
The MCP server uses token-based authentication:
24+
- DockerBuild.ps1 generates a random 128-bit secret when starting the MCP server
25+
- The secret is passed to the container via the `MCP_APPROVAL_SERVER_TOKEN` environment variable
26+
- All MCP requests include this secret as a URL parameter for authentication
27+
- The MCP server validates the token before processing any commands
28+
2129
### When to Use the MCP Server
2230

2331
Use the `ExecuteCommand` tool from the `host-approval` MCP server for:
@@ -48,7 +56,6 @@ Use the `ExecuteCommand` tool from the `host-approval` MCP server for:
4856
### How to Use
4957

5058
Call the `ExecuteCommand` tool with:
51-
- `sessionId`: Use a consistent session ID for your conversation (e.g., "session-{timestamp}")
5259
- `command`: The command to execute
5360
- `workingDirectory`: The working directory (use forward slashes: `X:/src/RepoName`)
5461
- `claimedPurpose`: A clear explanation of why this command is needed
@@ -58,7 +65,6 @@ Call the `ExecuteCommand` tool with:
5865
To push a feature branch:
5966
```
6067
ExecuteCommand(
61-
sessionId: "session-20241214",
6268
command: "git push origin feature/my-feature",
6369
workingDirectory: "X:/src/PostSharp.Engineering",
6470
claimedPurpose: "Push the feature branch with MCP implementation to remote for PR creation"

src/PostSharp.Engineering.BuildTools/Docker/ClaudeComponent.cs

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,61 @@ public class ClaudeComponent : ContainerComponent
1515

1616
public override ContainerComponentKind Kind => ContainerComponentKind.Claude;
1717

18+
/// <summary>
19+
/// Gets the list of marketplace URLs to add in the container.
20+
/// These should be GitHub repository URLs (e.g., https://github.com/org/repo).
21+
/// Marketplaces are added first, then plugins can be installed from them.
22+
/// </summary>
23+
public string[] Marketplaces { get; init; } =
24+
[
25+
"https://github.com/metalama/Metalama.AI.Skills",
26+
"https://github.com/postsharp/PostSharp.Engineering.AISkills"
27+
];
28+
29+
/// <summary>
30+
/// Gets the list of plugin names to install from the added marketplaces.
31+
/// </summary>
32+
public string[] Plugins { get; init; } =
33+
[
34+
"metalama",
35+
"metalama-dev",
36+
"eng"
37+
];
38+
1839
public override void WriteDockerfile( TextWriter writer )
1940
{
2041
writer.WriteLine(
2142
"""
2243
# Configure npm global directory to avoid Windows container path issues
23-
ENV NPM_CONFIG_PREFIX=C:\npm
24-
ENV PATH="C:\npm;${PATH}"
44+
ENV NPM_CONFIG_PREFIX=C:\\npm
45+
ENV PATH="C:\\npm;${PATH}"
2546
2647
# Install Claude CLI using cmd shell to avoid HCS issues with PowerShell
2748
SHELL ["cmd", "/S", "/C"]
28-
RUN C:\nodejs\npm.cmd install --global @anthropic-ai/claude-code
49+
RUN C:\\nodejs\\npm.cmd install --global @anthropic-ai/claude-code
50+
51+
# Set HOME/USERPROFILE so Claude CLI finds credentials during build
52+
ENV HOME=C:\\Users\\ContainerUser
53+
ENV USERPROFILE=C:\\Users\\ContainerUser
54+
55+
# Create Claude config directory (credentials are mounted at runtime)
56+
RUN mkdir C:\Users\ContainerUser\.claude
57+
""" );
58+
59+
// Add marketplaces if any are specified
60+
foreach ( var marketplace in this.Marketplaces )
61+
{
62+
writer.WriteLine( $"RUN C:\\npm\\claude.cmd plugin marketplace add {marketplace}" );
63+
}
64+
65+
// Install plugins from the added marketplaces
66+
foreach ( var plugin in this.Plugins )
67+
{
68+
writer.WriteLine( $"RUN C:\\npm\\claude.cmd plugin install {plugin}" );
69+
}
70+
71+
writer.WriteLine(
72+
"""
2973
3074
# Restore PowerShell shell using full path
3175
SHELL ["C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", "-Command"]
@@ -55,4 +99,4 @@ public override void AddRequirements( IReadOnlyList<ContainerComponent> componen
5599
add( new GitHubCliComponent() );
56100
}
57101
}
58-
}
102+
}

src/PostSharp.Engineering.BuildTools/Mcp/McpServerCommand.cs

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -63,16 +63,26 @@ public override async Task<int> ExecuteAsync( CommandContext context, McpServerC
6363

6464
var app = builder.Build();
6565

66-
// Add request logging middleware
67-
app.Use( async ( httpContext, next ) =>
66+
// Add request logging middleware (only if verbose mode is enabled)
67+
if ( settings.Verbose )
6868
{
69-
AnsiConsole.MarkupLine( $"[dim]HTTP {httpContext.Request.Method} {httpContext.Request.Path}[/]" );
69+
app.Use( async ( httpContext, next ) =>
70+
{
71+
AnsiConsole.MarkupLine( $"[dim]HTTP {httpContext.Request.Method} {httpContext.Request.Path}[/]" );
7072

71-
await next();
72-
} );
73+
await next();
74+
} );
75+
}
7376

74-
// Map MCP endpoints
75-
app.MapMcp();
77+
// Map MCP endpoints with token as base path (if configured)
78+
if ( !string.IsNullOrEmpty( settings.Secret ) )
79+
{
80+
app.MapMcp( $"/{settings.Secret}" );
81+
}
82+
else
83+
{
84+
app.MapMcp();
85+
}
7686

7787
// Start the server
7888
await app.StartAsync();
@@ -92,9 +102,6 @@ public override async Task<int> ExecuteAsync( CommandContext context, McpServerC
92102
AnsiConsole.MarkupLine( "[dim]Press Ctrl+C to stop the server[/]" );
93103
AnsiConsole.WriteLine();
94104

95-
// Write to stdout for programmatic consumption
96-
Console.WriteLine( $"MCP_SERVER_PORT={actualPort}" );
97-
98105
// Wait for shutdown signal
99106
await app.WaitForShutdownAsync();
100107

src/PostSharp.Engineering.BuildTools/Mcp/McpServerCommandSettings.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,13 @@ public sealed class McpServerCommandSettings : CommandSettings
1818
[CommandOption( "--port-file" )]
1919
[Description( "File path to write the assigned port number. Used for dynamic port discovery." )]
2020
public string? PortFile { get; init; }
21+
22+
[CommandOption( "--verbose" )]
23+
[Description( "Enable verbose logging including HTTP requests." )]
24+
[DefaultValue( false )]
25+
public bool Verbose { get; init; }
26+
27+
[CommandOption( "--secret" )]
28+
[Description( "Security token for authenticating requests. Required for production use." )]
29+
public string? Secret { get; init; }
2130
}

src/PostSharp.Engineering.BuildTools/Mcp/Tools/ExecuteCommandTool.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,6 @@ public ExecuteCommandTool(
3737
[McpServerTool]
3838
[Description( "Execute a PowerShell command on the host machine. Requires human approval. Use this for git push, GitHub operations, and other actions that affect external systems or require privileges or tokens that the container does not have." )]
3939
public async Task<CommandResult> ExecuteCommand(
40-
[Description( "Unique session identifier for tracking command history" )]
41-
string sessionId,
4240
[Description( "The command to execute (e.g., 'git push origin main'). Must be valid PowerShell script." )]
4341
string command,
4442
[Description( "The working directory for command execution" )]
@@ -47,11 +45,13 @@ public async Task<CommandResult> ExecuteCommand(
4745
string claimedPurpose,
4846
CancellationToken cancellationToken = default )
4947
{
48+
// Use a constant session ID for single session model
49+
const string sessionId = "default";
50+
5051
// Log incoming request
5152
AnsiConsole.WriteLine();
5253
AnsiConsole.Write( new Rule( "[yellow]Incoming Command Request[/]" ) );
5354
AnsiConsole.MarkupLine( $"[dim]Time:[/] {DateTime.Now:yyyy-MM-dd HH:mm:ss}" );
54-
AnsiConsole.MarkupLine( $"[dim]Session:[/] {sessionId}" );
5555
AnsiConsole.MarkupLine( $"[dim]Command:[/] [white]{command.EscapeMarkup()}[/]" );
5656
AnsiConsole.MarkupLine( $"[dim]Working Directory:[/] {workingDirectory.EscapeMarkup()}" );
5757
AnsiConsole.MarkupLine( $"[dim]Purpose:[/] {claimedPurpose.EscapeMarkup()}" );

src/PostSharp.Engineering.BuildTools/Resources/DockerBuild.ps1

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -560,12 +560,19 @@ if (-not $BuildImage)
560560
# Start MCP approval server on host with dynamic port in new terminal tab
561561
$mcpPort = $null
562562
$mcpPortFile = $null
563+
$mcpSecret = $null
563564
if (-not $NoMcp) {
564565
Write-Host "Starting MCP approval server..." -ForegroundColor Green
565566
$mcpPortFile = Join-Path $env:TEMP "mcp-port-$([System.Guid]::NewGuid().ToString('N').Substring(0,8)).txt"
566567

568+
# Generate 128-bit (16 byte) random secret for authentication
569+
$randomBytes = New-Object byte[] 16
570+
[Security.Cryptography.RandomNumberGenerator]::Fill($randomBytes)
571+
$mcpSecret = [Convert]::ToBase64String($randomBytes)
572+
Write-Host "Generated MCP authentication secret" -ForegroundColor Cyan
573+
567574
# Build the command to run in the new tab
568-
$mcpCommand = "& '$SourceDirName\Build.ps1' tools mcp-server --port-file '$mcpPortFile'"
575+
$mcpCommand = "& '$SourceDirName\Build.ps1' tools mcp-server --port-file '$mcpPortFile' --secret '$mcpSecret'"
569576

570577
# Try Windows Terminal first (wt.exe), fall back to conhost
571578
$wtPath = Get-Command wt.exe -ErrorAction SilentlyContinue
@@ -676,6 +683,11 @@ if (-not $BuildImage)
676683
"-e", "USERPROFILE=$containerUserProfile"
677684
)
678685

686+
# Pass MCP secret to container if MCP server is running
687+
if ($mcpSecret) {
688+
$envArgs += @("-e", "MCP_APPROVAL_SERVER_TOKEN=$mcpSecret")
689+
}
690+
679691
Write-Host "Executing: ``docker run --rm --memory=12g $dockerArgsAsString $VolumeMappingsAsString -e HOME=$containerUserProfile -e USERPROFILE=$containerUserProfile -w $ContainerSourceDir $ImageTag `"$pwshPath`" -Command `"$inlineScript`"" -ForegroundColor Cyan
680692

681693
try {

src/PostSharp.Engineering.BuildTools/Resources/RunClaude.ps1

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,17 @@ if ($env:RUNNING_IN_DOCKER -ne "true")
1717
# Configure MCP approval server if port is specified
1818
$mcpConfigArg = ""
1919
if ($McpPort -gt 0) {
20-
$sseUrl = "http://host.docker.internal:$McpPort/sse"
21-
Write-Host "Configuring MCP approval server: $sseUrl" -ForegroundColor Cyan
20+
# Get MCP secret from environment variable
21+
$mcpSecret = $env:MCP_APPROVAL_SERVER_TOKEN
22+
if ([string]::IsNullOrEmpty($mcpSecret)) {
23+
Write-Error "MCP_APPROVAL_SERVER_TOKEN environment variable is not set. Cannot authenticate to MCP server."
24+
exit 1
25+
}
26+
27+
# URL-encode the secret for path segment
28+
$encodedSecret = [System.Web.HttpUtility]::UrlEncode($mcpSecret)
29+
$sseUrl = "http://host.docker.internal:$McpPort/$encodedSecret/sse"
30+
Write-Host "Configuring MCP approval server (authenticated)" -ForegroundColor Cyan
2231

2332
# Create temporary MCP config file
2433
$mcpConfigPath = "$env:TEMP\mcp-config.json"

0 commit comments

Comments
 (0)