diff --git a/src/Deckster.McpServer.Tests/Deckster.McpServer.Tests.csproj b/src/Deckster.McpServer.Tests/Deckster.McpServer.Tests.csproj
new file mode 100644
index 00000000..b39fe2b6
--- /dev/null
+++ b/src/Deckster.McpServer.Tests/Deckster.McpServer.Tests.csproj
@@ -0,0 +1,31 @@
+
+
+
+ net9.0
+ enable
+ true
+ enable
+ latest
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Deckster.McpServer.Tests/Tools/DecksterGameToolsTests.cs b/src/Deckster.McpServer.Tests/Tools/DecksterGameToolsTests.cs
new file mode 100644
index 00000000..d2fa098d
--- /dev/null
+++ b/src/Deckster.McpServer.Tests/Tools/DecksterGameToolsTests.cs
@@ -0,0 +1,438 @@
+using Deckster.McpServer.Tools;
+using FluentAssertions;
+using System.Reflection;
+using ModelContextProtocol.Server;
+
+namespace Deckster.McpServer.Tests.Tools;
+
+[TestFixture]
+public class DecksterGameToolsTests
+{
+ [Test]
+ public async Task ListAvailableGames_ReturnsListOfGames()
+ {
+ // Arrange
+ var tools = new DecksterGameTools();
+
+ // Act
+ var result = await tools.ListAvailableGames();
+
+ // Assert
+ result.Should().Contain("CrazyEights");
+ }
+
+ [Test]
+ public void ListAvailableGames_HasMcpToolAttribute()
+ {
+ // Arrange
+ var method = typeof(DecksterGameTools).GetMethod(nameof(DecksterGameTools.ListAvailableGames));
+
+ // Act
+ var attribute = method?.GetCustomAttribute();
+
+ // Assert
+ attribute.Should().NotBeNull("the method should be marked as an MCP tool");
+ }
+
+ [Test]
+ public void ListAvailableGames_HasDescriptionAttribute()
+ {
+ // Arrange
+ var method = typeof(DecksterGameTools).GetMethod(nameof(DecksterGameTools.ListAvailableGames));
+
+ // Act
+ var attribute = method?.GetCustomAttribute();
+
+ // Assert
+ attribute.Should().NotBeNull("the method should have a description");
+ attribute?.Description.Should().NotBeNullOrEmpty();
+ }
+
+ [Test]
+ public void DecksterGameTools_HasMcpToolTypeAttribute()
+ {
+ // Arrange & Act
+ var attribute = typeof(DecksterGameTools).GetCustomAttribute();
+
+ // Assert
+ attribute.Should().NotBeNull("the class should be marked as an MCP tool type");
+ }
+
+ [Test]
+ public void CreateGameAsync_HasMcpToolAttribute()
+ {
+ // Arrange
+ var method = typeof(DecksterGameTools).GetMethod(nameof(DecksterGameTools.CreateGameAsync));
+
+ // Act
+ var attribute = method?.GetCustomAttribute();
+
+ // Assert
+ attribute.Should().NotBeNull("the method should be marked as an MCP tool");
+ }
+
+ [Test]
+ public void CreateGameAsync_HasDescriptionAttribute()
+ {
+ // Arrange
+ var method = typeof(DecksterGameTools).GetMethod(nameof(DecksterGameTools.CreateGameAsync));
+
+ // Act
+ var attribute = method?.GetCustomAttribute();
+
+ // Assert
+ attribute.Should().NotBeNull("the method should have a description");
+ attribute?.Description.Should().Contain("game");
+ }
+
+ [Test]
+ public void AddBotAsync_HasMcpToolAttribute()
+ {
+ // Arrange
+ var method = typeof(DecksterGameTools).GetMethod(nameof(DecksterGameTools.AddBotAsync));
+
+ // Act
+ var attribute = method?.GetCustomAttribute();
+
+ // Assert
+ attribute.Should().NotBeNull("the method should be marked as an MCP tool");
+ }
+
+ [Test]
+ public void AddBotAsync_HasDescriptionAttribute()
+ {
+ // Arrange
+ var method = typeof(DecksterGameTools).GetMethod(nameof(DecksterGameTools.AddBotAsync));
+
+ // Act
+ var attribute = method?.GetCustomAttribute();
+
+ // Assert
+ attribute.Should().NotBeNull("the method should have a description");
+ attribute?.Description.Should().Contain("bot");
+ }
+
+ [Test]
+ public void StartGameAsync_HasMcpToolAttribute()
+ {
+ // Arrange
+ var method = typeof(DecksterGameTools).GetMethod(nameof(DecksterGameTools.StartGameAsync));
+
+ // Act
+ var attribute = method?.GetCustomAttribute();
+
+ // Assert
+ attribute.Should().NotBeNull("the method should be marked as an MCP tool");
+ }
+
+ [Test]
+ public void StartGameAsync_HasDescriptionAttribute()
+ {
+ // Arrange
+ var method = typeof(DecksterGameTools).GetMethod(nameof(DecksterGameTools.StartGameAsync));
+
+ // Act
+ var attribute = method?.GetCustomAttribute();
+
+ // Assert
+ attribute.Should().NotBeNull("the method should have a description");
+ attribute?.Description.Should().Contain("Start");
+ }
+
+ [Test]
+ public void GetPreviousGamesAsync_HasMcpToolAttribute()
+ {
+ // Arrange
+ var method = typeof(DecksterGameTools).GetMethod(nameof(DecksterGameTools.GetPreviousGamesAsync));
+
+ // Act
+ var attribute = method?.GetCustomAttribute();
+
+ // Assert
+ attribute.Should().NotBeNull("the method should be marked as an MCP tool");
+ }
+
+ [Test]
+ public void GetPreviousGamesAsync_HasDescriptionAttribute()
+ {
+ // Arrange
+ var method = typeof(DecksterGameTools).GetMethod(nameof(DecksterGameTools.GetPreviousGamesAsync));
+
+ // Act
+ var attribute = method?.GetCustomAttribute();
+
+ // Assert
+ attribute.Should().NotBeNull("the method should have a description");
+ attribute?.Description.Should().Contain("previous");
+ }
+
+ [Test]
+ public void GetActiveGamesAsync_HasMcpToolAttribute()
+ {
+ // Arrange
+ var method = typeof(DecksterGameTools).GetMethod(nameof(DecksterGameTools.GetActiveGamesAsync));
+
+ // Act
+ var attribute = method?.GetCustomAttribute();
+
+ // Assert
+ attribute.Should().NotBeNull("the method should be marked as an MCP tool");
+ }
+
+ [Test]
+ public void GetActiveGamesAsync_HasDescriptionAttribute()
+ {
+ // Arrange
+ var method = typeof(DecksterGameTools).GetMethod(nameof(DecksterGameTools.GetActiveGamesAsync));
+
+ // Act
+ var attribute = method?.GetCustomAttribute();
+
+ // Assert
+ attribute.Should().NotBeNull("the method should have a description");
+ attribute?.Description.Should().Contain("active");
+ }
+
+ [Test]
+ public void GetGameStateAsync_HasMcpToolAttribute()
+ {
+ // Arrange
+ var method = typeof(DecksterGameTools).GetMethod(nameof(DecksterGameTools.GetGameStateAsync));
+
+ // Act
+ var attribute = method?.GetCustomAttribute();
+
+ // Assert
+ attribute.Should().NotBeNull("the method should be marked as an MCP tool");
+ }
+
+ [Test]
+ public void GetGameStateAsync_HasDescriptionAttribute()
+ {
+ // Arrange
+ var method = typeof(DecksterGameTools).GetMethod(nameof(DecksterGameTools.GetGameStateAsync));
+
+ // Act
+ var attribute = method?.GetCustomAttribute();
+
+ // Assert
+ attribute.Should().NotBeNull("the method should have a description");
+ attribute?.Description.Should().Contain("state");
+ }
+
+ [Test]
+ public void DeleteGameAsync_HasMcpToolAttribute()
+ {
+ // Arrange
+ var method = typeof(DecksterGameTools).GetMethod(nameof(DecksterGameTools.DeleteGameAsync));
+
+ // Act
+ var attribute = method?.GetCustomAttribute();
+
+ // Assert
+ attribute.Should().NotBeNull("the method should be marked as an MCP tool");
+ }
+
+ [Test]
+ public void DeleteGameAsync_HasDescriptionAttribute()
+ {
+ // Arrange
+ var method = typeof(DecksterGameTools).GetMethod(nameof(DecksterGameTools.DeleteGameAsync));
+
+ // Act
+ var attribute = method?.GetCustomAttribute();
+
+ // Assert
+ attribute.Should().NotBeNull("the method should have a description");
+ attribute?.Description.Should().ContainEquivalentOf("delete");
+ }
+
+ [Test]
+ public void GetGameOverviewAsync_HasMcpToolAttribute()
+ {
+ // Arrange
+ var method = typeof(DecksterGameTools).GetMethod(nameof(DecksterGameTools.GetGameOverviewAsync));
+
+ // Act
+ var attribute = method?.GetCustomAttribute();
+
+ // Assert
+ attribute.Should().NotBeNull("the method should be marked as an MCP tool");
+ }
+
+ [Test]
+ public void GetGameOverviewAsync_HasDescriptionAttribute()
+ {
+ // Arrange
+ var method = typeof(DecksterGameTools).GetMethod(nameof(DecksterGameTools.GetGameOverviewAsync));
+
+ // Act
+ var attribute = method?.GetCustomAttribute();
+
+ // Assert
+ attribute.Should().NotBeNull("the method should have a description");
+ attribute?.Description.Should().ContainAny("overview", "Overview");
+ }
+
+ [Test]
+ public void GetHistoricalGameAsync_HasMcpToolAttribute()
+ {
+ // Arrange
+ var method = typeof(DecksterGameTools).GetMethod(nameof(DecksterGameTools.GetHistoricalGameAsync));
+
+ // Act
+ var attribute = method?.GetCustomAttribute();
+
+ // Assert
+ attribute.Should().NotBeNull("the method should be marked as an MCP tool");
+ }
+
+ [Test]
+ public void GetHistoricalGameAsync_HasDescriptionAttribute()
+ {
+ // Arrange
+ var method = typeof(DecksterGameTools).GetMethod(nameof(DecksterGameTools.GetHistoricalGameAsync));
+
+ // Act
+ var attribute = method?.GetCustomAttribute();
+
+ // Assert
+ attribute.Should().NotBeNull("the method should have a description");
+ attribute?.Description.Should().ContainAny("historical", "Historical");
+ }
+
+ [Test]
+ public void GetHistoricalGameVersionAsync_HasMcpToolAttribute()
+ {
+ // Arrange
+ var method = typeof(DecksterGameTools).GetMethod(nameof(DecksterGameTools.GetHistoricalGameVersionAsync));
+
+ // Act
+ var attribute = method?.GetCustomAttribute();
+
+ // Assert
+ attribute.Should().NotBeNull("the method should be marked as an MCP tool");
+ }
+
+ [Test]
+ public void GetHistoricalGameVersionAsync_HasDescriptionAttribute()
+ {
+ // Arrange
+ var method = typeof(DecksterGameTools).GetMethod(nameof(DecksterGameTools.GetHistoricalGameVersionAsync));
+
+ // Act
+ var attribute = method?.GetCustomAttribute();
+
+ // Assert
+ attribute.Should().NotBeNull("the method should have a description");
+ attribute?.Description.Should().ContainAny("version", "Version");
+ }
+
+ [Test]
+ public void GetGameMetadataAsync_HasMcpToolAttribute()
+ {
+ // Arrange
+ var method = typeof(DecksterGameTools).GetMethod(nameof(DecksterGameTools.GetGameMetadataAsync));
+
+ // Act
+ var attribute = method?.GetCustomAttribute();
+
+ // Assert
+ attribute.Should().NotBeNull("the method should be marked as an MCP tool");
+ }
+
+ [Test]
+ public void GetGameMetadataAsync_HasDescriptionAttribute()
+ {
+ // Arrange
+ var method = typeof(DecksterGameTools).GetMethod(nameof(DecksterGameTools.GetGameMetadataAsync));
+
+ // Act
+ var attribute = method?.GetCustomAttribute();
+
+ // Assert
+ attribute.Should().NotBeNull("the method should have a description");
+ attribute?.Description.Should().ContainAny("metadata", "Metadata");
+ }
+
+ [Test]
+ public void GetGameDescriptionAsync_HasMcpToolAttribute()
+ {
+ // Arrange
+ var method = typeof(DecksterGameTools).GetMethod(nameof(DecksterGameTools.GetGameDescriptionAsync));
+
+ // Act
+ var attribute = method?.GetCustomAttribute();
+
+ // Assert
+ attribute.Should().NotBeNull("the method should be marked as an MCP tool");
+ }
+
+ [Test]
+ public void GetGameDescriptionAsync_HasDescriptionAttribute()
+ {
+ // Arrange
+ var method = typeof(DecksterGameTools).GetMethod(nameof(DecksterGameTools.GetGameDescriptionAsync));
+
+ // Act
+ var attribute = method?.GetCustomAttribute();
+
+ // Assert
+ attribute.Should().NotBeNull("the method should have a description");
+ attribute?.Description.Should().ContainAny("description", "Description");
+ }
+
+ [Test]
+ public void GetCurrentUserAsync_HasMcpToolAttribute()
+ {
+ // Arrange
+ var method = typeof(DecksterGameTools).GetMethod(nameof(DecksterGameTools.GetCurrentUserAsync));
+
+ // Act
+ var attribute = method?.GetCustomAttribute();
+
+ // Assert
+ attribute.Should().NotBeNull("the method should be marked as an MCP tool");
+ }
+
+ [Test]
+ public void GetCurrentUserAsync_HasDescriptionAttribute()
+ {
+ // Arrange
+ var method = typeof(DecksterGameTools).GetMethod(nameof(DecksterGameTools.GetCurrentUserAsync));
+
+ // Act
+ var attribute = method?.GetCustomAttribute();
+
+ // Assert
+ attribute.Should().NotBeNull("the method should have a description");
+ attribute?.Description.Should().ContainAny("user", "User");
+ }
+
+ [Test]
+ public void GetMessageMetadataAsync_HasMcpToolAttribute()
+ {
+ // Arrange
+ var method = typeof(DecksterGameTools).GetMethod(nameof(DecksterGameTools.GetMessageMetadataAsync));
+
+ // Act
+ var attribute = method?.GetCustomAttribute();
+
+ // Assert
+ attribute.Should().NotBeNull("the method should be marked as an MCP tool");
+ }
+
+ [Test]
+ public void GetMessageMetadataAsync_HasDescriptionAttribute()
+ {
+ // Arrange
+ var method = typeof(DecksterGameTools).GetMethod(nameof(DecksterGameTools.GetMessageMetadataAsync));
+
+ // Act
+ var attribute = method?.GetCustomAttribute();
+
+ // Assert
+ attribute.Should().NotBeNull("the method should have a description");
+ attribute?.Description.Should().ContainAny("message", "Message");
+ }
+}
diff --git a/src/Deckster.McpServer.Tests/Wrappers/DecksterClientWrapperTests.cs b/src/Deckster.McpServer.Tests/Wrappers/DecksterClientWrapperTests.cs
new file mode 100644
index 00000000..15204984
--- /dev/null
+++ b/src/Deckster.McpServer.Tests/Wrappers/DecksterClientWrapperTests.cs
@@ -0,0 +1,78 @@
+using Deckster.Client;
+using Deckster.McpServer.Wrappers;
+using FluentAssertions;
+
+namespace Deckster.McpServer.Tests.Wrappers;
+
+[TestFixture]
+public class DecksterClientWrapperTests
+{
+ [Test]
+ public void Constructor_NullClient_ThrowsArgumentNullException()
+ {
+ // Arrange & Act
+ var action = () => new DecksterClientWrapper(null!, new HttpClient(), "http://localhost:13992");
+
+ // Assert
+ action.Should().Throw()
+ .WithParameterName("client");
+ }
+
+ [Test]
+ public void Constructor_ValidClient_CreatesInstance()
+ {
+ // Arrange
+ var client = new DecksterClient("http://localhost:13992", "test-token");
+ var httpClient = new HttpClient();
+
+ // Act
+ var wrapper = new DecksterClientWrapper(client, httpClient, "http://localhost:13992");
+
+ // Assert
+ wrapper.Should().NotBeNull();
+ }
+
+ [Test]
+ public void CreateGameAsync_MethodExists()
+ {
+ // Arrange
+ var method = typeof(DecksterClientWrapper).GetMethod(nameof(DecksterClientWrapper.CreateGameAsync));
+
+ // Assert
+ method.Should().NotBeNull("CreateGameAsync method should exist");
+ method?.ReturnType.Should().Be(typeof(Task), "should return Task");
+ }
+
+ [Test]
+ public void AddBotAsync_MethodExists()
+ {
+ // Arrange
+ var method = typeof(DecksterClientWrapper).GetMethod(nameof(DecksterClientWrapper.AddBotAsync));
+
+ // Assert
+ method.Should().NotBeNull("AddBotAsync method should exist");
+ method?.ReturnType.Should().Be(typeof(Task), "should return Task");
+ }
+
+ [Test]
+ public void StartGameAsync_MethodExists()
+ {
+ // Arrange
+ var method = typeof(DecksterClientWrapper).GetMethod(nameof(DecksterClientWrapper.StartGameAsync));
+
+ // Assert
+ method.Should().NotBeNull("StartGameAsync method should exist");
+ method?.ReturnType.Should().Be(typeof(Task), "should return Task");
+ }
+
+ [Test]
+ public void GetPreviousGamesAsync_MethodExists()
+ {
+ // Arrange
+ var method = typeof(DecksterClientWrapper).GetMethod(nameof(DecksterClientWrapper.GetPreviousGamesAsync));
+
+ // Assert
+ method.Should().NotBeNull("GetPreviousGamesAsync method should exist");
+ method?.ReturnType.Should().Be(typeof(Task), "should return Task");
+ }
+}
diff --git a/src/Deckster.McpServer/Configuration/DecksterConfiguration.cs b/src/Deckster.McpServer/Configuration/DecksterConfiguration.cs
new file mode 100644
index 00000000..c6e79f9b
--- /dev/null
+++ b/src/Deckster.McpServer/Configuration/DecksterConfiguration.cs
@@ -0,0 +1,18 @@
+namespace Deckster.McpServer.Configuration;
+
+public class DecksterConfiguration
+{
+ public string ServerUrl { get; set; } = "http://localhost:13992";
+ public string Username { get; set; } = string.Empty;
+ public string Password { get; set; } = string.Empty;
+
+ public static DecksterConfiguration FromEnvironment()
+ {
+ return new DecksterConfiguration
+ {
+ ServerUrl = Environment.GetEnvironmentVariable("DECKSTER_SERVER_URL") ?? "http://localhost:13992",
+ Username = Environment.GetEnvironmentVariable("DECKSTER_USERNAME") ?? string.Empty,
+ Password = Environment.GetEnvironmentVariable("DECKSTER_PASSWORD") ?? string.Empty
+ };
+ }
+}
diff --git a/src/Deckster.McpServer/Deckster.McpServer.csproj b/src/Deckster.McpServer/Deckster.McpServer.csproj
new file mode 100644
index 00000000..1b95c75e
--- /dev/null
+++ b/src/Deckster.McpServer/Deckster.McpServer.csproj
@@ -0,0 +1,23 @@
+
+
+
+ Exe
+ net9.0
+ enable
+ true
+ enable
+ latest
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Deckster.McpServer/Program.cs b/src/Deckster.McpServer/Program.cs
new file mode 100644
index 00000000..c9b8296b
--- /dev/null
+++ b/src/Deckster.McpServer/Program.cs
@@ -0,0 +1,42 @@
+using Deckster.McpServer.Configuration;
+using Deckster.McpServer.Tools;
+using Deckster.McpServer.Wrappers;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using ModelContextProtocol;
+
+namespace Deckster.McpServer;
+
+public class Program
+{
+ public static async Task Main(string[] args)
+ {
+ var configuration = DecksterConfiguration.FromEnvironment();
+
+ var builder = Host.CreateApplicationBuilder(args);
+
+ // Configure logging
+ builder.Logging.ClearProviders();
+ builder.Logging.AddConsole();
+ builder.Logging.SetMinimumLevel(LogLevel.Information);
+
+ // Register services
+ builder.Services.AddSingleton(configuration);
+
+ // Create and register DecksterClientWrapper asynchronously
+ var clientWrapper = await DecksterClientWrapper.CreateAsync(configuration);
+ builder.Services.AddSingleton(clientWrapper);
+
+ // Register DecksterGameTools
+ builder.Services.AddSingleton();
+
+ // Configure MCP server
+ builder.Services.AddMcpServer()
+ .WithStdioServerTransport()
+ .WithToolsFromAssembly();
+
+ var host = builder.Build();
+ await host.RunAsync();
+ }
+}
diff --git a/src/Deckster.McpServer/README.md b/src/Deckster.McpServer/README.md
new file mode 100644
index 00000000..535b4a5b
--- /dev/null
+++ b/src/Deckster.McpServer/README.md
@@ -0,0 +1,293 @@
+# Deckster MCP Server
+
+A Model Context Protocol (MCP) server that enables AI coding assistants like Claude Code, GitHub Copilot, and Cursor to interact with the Deckster card game platform.
+
+## Overview
+
+The Deckster MCP Server provides programmatic access to the Deckster card game platform through the Model Context Protocol. This allows AI assistants to:
+
+- 🎴 Browse available card games
+- 🎮 Create and manage game sessions
+- 🤖 Add AI players to games
+- ▶️ Control game flow
+- 🎯 Automate testing of multiplayer games
+
+## Features
+
+### Game Management
+- List all available card games (CrazyEights, Bullshit, Idiot, Uno, Yaniv, Gabong, TexasHoldEm)
+- Create new game sessions with custom names
+- Add AI bot players with configurable difficulty
+- Start, pause, and manage game state
+
+### Supported Games
+- **CrazyEights** - Fully implemented with complete API support
+- **Other games** - Listed with basic support (implementation in progress)
+
+## Requirements
+
+- **.NET 9.0 SDK** or higher
+- **Deckster Server** running (default: http://localhost:13992)
+- Valid Deckster account credentials
+- MCP-compatible client (Claude Code, GitHub Copilot, Cursor)
+
+## Installation
+
+### For Claude Code
+
+Install using the Claude Code CLI:
+
+```bash
+# Coming soon - package publication pending
+claude mcp add deckster dotnet run --project /path/to/Deckster.McpServer.csproj
+```
+
+### Manual Installation
+
+1. **Build the MCP Server**
+```bash
+cd src/Deckster.McpServer
+dotnet build
+```
+
+2. **Configure your MCP client**
+
+Add to your MCP configuration:
+
+```json
+{
+ "mcpServers": {
+ "deckster": {
+ "command": "dotnet",
+ "args": [
+ "run",
+ "--project",
+ "/absolute/path/to/deckster/src/Deckster.McpServer/Deckster.McpServer.csproj"
+ ],
+ "env": {
+ "DECKSTER_SERVER_URL": "http://localhost:13992",
+ "DECKSTER_USERNAME": "your-username",
+ "DECKSTER_PASSWORD": "your-password"
+ }
+ }
+ }
+}
+```
+
+### For GitHub Copilot / Cursor
+
+Configure as an external tool or extension following your IDE's MCP integration guidelines.
+
+## Usage with AI Assistants
+
+### Claude Code
+Once configured, you can use natural language commands:
+
+```
+"What card games are available on Deckster?"
+"Create a CrazyEights game called 'test-game' with 3 bots and start it"
+"Set up automated testing for my card game implementation"
+```
+
+### GitHub Copilot
+Use inline comments to trigger Copilot suggestions:
+
+```csharp
+// Use Deckster MCP to create a new CrazyEights game with 2 bots
+// Use Deckster MCP to list all available games
+```
+
+### Cursor
+Ask Cursor to interact with your Deckster server for testing and development:
+
+```
+"Help me test my new card game implementation by creating games and adding bots"
+"Generate test scenarios using the Deckster MCP server"
+```
+
+## MCP Tools
+
+### Available Tools
+
+| Tool | Description | Parameters |
+|------|-------------|------------|
+| `ListAvailableGames` | Lists all available card games | None |
+| `CreateGameAsync` | Creates a new game session | `gameType`, `gameName` |
+| `AddBotAsync` | Adds an AI bot to a game | `gameType`, `gameId` |
+| `StartGameAsync` | Starts a game session | `gameType`, `gameId` |
+
+### Tool Categories
+
+#### Game Discovery
+- `ListAvailableGames` - Discover what games are available on the server
+
+#### Game Management
+- `CreateGameAsync` - Initialize new game sessions
+- `AddBotAsync` - Populate games with AI players
+- `StartGameAsync` - Begin gameplay
+
+#### Coming Soon
+- `GetGameState` - Retrieve current game state
+- `MakeMove` - Execute player actions
+- `GetGameHistory` - Access game logs
+- `ConfigureBot` - Adjust AI difficulty settings
+
+## Configuration
+
+### Environment Variables
+
+| Variable | Default | Description |
+|----------|---------|-------------|
+| `DECKSTER_SERVER_URL` | `http://localhost:13992` | URL of your Deckster server |
+| `DECKSTER_USERNAME` | (required) | Your Deckster username |
+| `DECKSTER_PASSWORD` | (required) | Your Deckster password |
+
+### Advanced Configuration
+
+You can also configure the server for different scenarios:
+
+```json
+{
+ "mcpServers": {
+ "deckster-local": {
+ "command": "dotnet",
+ "args": ["run", "--project", "/path/to/Deckster.McpServer.csproj"],
+ "env": {
+ "DECKSTER_SERVER_URL": "http://localhost:13992",
+ "DECKSTER_USERNAME": "test-user",
+ "DECKSTER_PASSWORD": "test-pass"
+ }
+ },
+ "deckster-remote": {
+ "command": "dotnet",
+ "args": ["run", "--project", "/path/to/Deckster.McpServer.csproj"],
+ "env": {
+ "DECKSTER_SERVER_URL": "https://deckster.example.com",
+ "DECKSTER_USERNAME": "prod-user",
+ "DECKSTER_PASSWORD": "prod-pass"
+ }
+ }
+ }
+}
+```
+
+## Development
+
+### Running Tests
+
+```bash
+cd src/Deckster.McpServer.Tests
+dotnet test
+```
+
+### Project Structure
+
+```
+src/Deckster.McpServer/
+├── Program.cs # Entry point and MCP server setup
+├── Configuration/ # Configuration models
+│ └── DecksterConfiguration.cs
+├── Tools/ # MCP tool implementations
+│ └── DecksterGameTools.cs
+├── Wrappers/ # Deckster client wrapper
+│ └── DecksterClientWrapper.cs
+└── README.md # This file
+```
+
+### Adding New Games
+
+To add support for more games:
+
+1. Implement the game-specific client in `Deckster.Client/Games/{GameName}/`
+2. Add corresponding methods in `DecksterClientWrapper`
+3. Update the `DecksterGameTools` with new MCP tool methods
+4. Add tests in `Deckster.McpServer.Tests`
+
+## Troubleshooting
+
+### MCP client can't see the Deckster tools
+
+1. **Configuration Issues**
+ - Verify the configuration file is valid JSON (no trailing commas)
+ - Check the absolute path to the project is correct
+ - For Claude Code: Restart the application completely
+ - For VS Code/Copilot: Reload the window
+
+2. **Connection Errors**
+ ```bash
+ # Verify Deckster server is running
+ curl http://localhost:13992
+
+ # Test with correct credentials
+ dotnet run -- --test-connection
+ ```
+
+3. **Authentication Failures**
+ - Credentials are case-sensitive
+ - Check for special characters that need escaping in JSON
+ - Verify account exists on Deckster server
+
+### Build Errors
+
+```bash
+# Verify .NET version
+dotnet --version # Should be 9.0+
+
+# Clean rebuild
+dotnet clean
+dotnet restore
+dotnet build
+
+# If packages fail to restore
+dotnet nuget locals all --clear
+```
+
+### Runtime Issues
+
+1. **Port conflicts**: Ensure port 13992 is not in use by another service
+2. **Firewall**: Allow localhost connections
+3. **Permissions**: Ensure read/write access to project directory
+
+## Security Considerations
+
+The MCP server requires credentials to access your Deckster server. These credentials are:
+- Stored in your local MCP configuration
+- Never transmitted outside of localhost connections
+- Used only for authenticated API calls to your Deckster server
+
+**Best Practices:**
+- Use dedicated MCP accounts with limited permissions
+- Rotate credentials regularly
+- Never commit configuration files with credentials to version control
+
+## License
+
+Part of the Deckster platform. See main repository for license details.
+
+## Contributing
+
+We welcome contributions! Areas where you can help:
+
+1. **Add More Games** - Implement MCP tools for Bullshit, Idiot, Uno, etc.
+2. **Enhance Tools** - Add game state queries, move execution, statistics
+3. **Improve Testing** - Expand test coverage for edge cases
+4. **Documentation** - Add examples and use cases
+
+See the main Deckster repository for contribution guidelines.
+
+## Resources
+
+- [Model Context Protocol Specification](https://github.com/modelcontextprotocol/specification)
+- [Deckster Main Repository](https://github.com/yourusername/deckster)
+- [MCP Server Examples](https://github.com/modelcontextprotocol/servers)
+
+## Support
+
+For issues or questions:
+- Open an issue in this repository
+- Check existing [documentation](../../docs/)
+- Contact the Deckster team
+
+---
+
+**Current Status**: MVP implementation with CrazyEights support. Actively expanding to support all Deckster games.
\ No newline at end of file
diff --git a/src/Deckster.McpServer/Tools/DecksterGameTools.cs b/src/Deckster.McpServer/Tools/DecksterGameTools.cs
new file mode 100644
index 00000000..951182c9
--- /dev/null
+++ b/src/Deckster.McpServer/Tools/DecksterGameTools.cs
@@ -0,0 +1,200 @@
+using System.ComponentModel;
+using Deckster.McpServer.Wrappers;
+using ModelContextProtocol.Server;
+
+namespace Deckster.McpServer.Tools;
+
+[McpServerToolType]
+public class DecksterGameTools
+{
+ private readonly DecksterClientWrapper? _clientWrapper;
+
+ private static readonly string[] AvailableGames =
+ [
+ "CrazyEights",
+ "Bullshit",
+ "Idiot",
+ "Uno",
+ "Yaniv",
+ "Gabong",
+ "TexasHoldEm"
+ ];
+
+ public DecksterGameTools(DecksterClientWrapper? clientWrapper = null)
+ {
+ _clientWrapper = clientWrapper;
+ }
+
+ [McpServerTool]
+ [Description("Lists all available card games that can be played on Deckster server")]
+ public Task ListAvailableGames() =>
+ Task.FromResult(string.Join("\n", AvailableGames));
+
+ [McpServerTool]
+ [Description("Creates a new game on the Deckster server. Returns the game ID.")]
+ public async Task CreateGameAsync(string gameType, string gameName)
+ {
+ if (_clientWrapper == null)
+ {
+ throw new InvalidOperationException("Client wrapper is not configured");
+ }
+
+ return await _clientWrapper.CreateGameAsync(gameType, gameName);
+ }
+
+ [McpServerTool]
+ [Description("Adds an AI bot player to an existing game")]
+ public async Task AddBotAsync(string gameType, string gameId)
+ {
+ if (_clientWrapper == null)
+ {
+ throw new InvalidOperationException("Client wrapper is not configured");
+ }
+
+ await _clientWrapper.AddBotAsync(gameType, gameId);
+ }
+
+ [McpServerTool]
+ [Description("Starts an existing game that has enough players")]
+ public async Task StartGameAsync(string gameType, string gameId)
+ {
+ if (_clientWrapper == null)
+ {
+ throw new InvalidOperationException("Client wrapper is not configured");
+ }
+
+ await _clientWrapper.StartGameAsync(gameType, gameId);
+ }
+
+ [McpServerTool]
+ [Description("Lists previous/historical games for a specific game type. Returns game data as JSON.")]
+ public async Task GetPreviousGamesAsync(string gameType)
+ {
+ if (_clientWrapper == null)
+ {
+ throw new InvalidOperationException("Client wrapper is not configured");
+ }
+
+ return await _clientWrapper.GetPreviousGamesAsync(gameType);
+ }
+
+ [McpServerTool]
+ [Description("Lists all currently active games for a specific game type. Returns game data as JSON.")]
+ public async Task GetActiveGamesAsync(string gameType)
+ {
+ if (_clientWrapper == null)
+ {
+ throw new InvalidOperationException("Client wrapper is not configured");
+ }
+
+ return await _clientWrapper.GetActiveGamesAsync(gameType);
+ }
+
+ [McpServerTool]
+ [Description("Gets the current state of a specific game. Returns detailed game state as JSON.")]
+ public async Task GetGameStateAsync(string gameType, string gameName)
+ {
+ if (_clientWrapper == null)
+ {
+ throw new InvalidOperationException("Client wrapper is not configured");
+ }
+
+ return await _clientWrapper.GetGameStateAsync(gameType, gameName);
+ }
+
+ [McpServerTool]
+ [Description("Deletes a game from the server. Use with caution as this permanently removes the game.")]
+ public async Task DeleteGameAsync(string gameType, string gameName)
+ {
+ if (_clientWrapper == null)
+ {
+ throw new InvalidOperationException("Client wrapper is not configured");
+ }
+
+ await _clientWrapper.DeleteGameAsync(gameType, gameName);
+ }
+
+ [McpServerTool]
+ [Description("Gets an overview of a specific game type, including available games and statistics. Returns data as JSON.")]
+ public async Task GetGameOverviewAsync(string gameType)
+ {
+ if (_clientWrapper == null)
+ {
+ throw new InvalidOperationException("Client wrapper is not configured");
+ }
+
+ return await _clientWrapper.GetGameOverviewAsync(gameType);
+ }
+
+ [McpServerTool]
+ [Description("Gets a specific historical/completed game by its ID. Returns full game history as JSON.")]
+ public async Task GetHistoricalGameAsync(string gameType, string gameId)
+ {
+ if (_clientWrapper == null)
+ {
+ throw new InvalidOperationException("Client wrapper is not configured");
+ }
+
+ return await _clientWrapper.GetHistoricalGameAsync(gameType, gameId);
+ }
+
+ [McpServerTool]
+ [Description("Gets a specific version/snapshot of a historical game. Returns game state at that version as JSON.")]
+ public async Task GetHistoricalGameVersionAsync(string gameType, string gameId, int version)
+ {
+ if (_clientWrapper == null)
+ {
+ throw new InvalidOperationException("Client wrapper is not configured");
+ }
+
+ return await _clientWrapper.GetHistoricalGameVersionAsync(gameType, gameId, version);
+ }
+
+ [McpServerTool]
+ [Description("Gets metadata information about a specific game type (rules, requirements, etc.). Returns metadata as JSON.")]
+ public async Task GetGameMetadataAsync(string gameType)
+ {
+ if (_clientWrapper == null)
+ {
+ throw new InvalidOperationException("Client wrapper is not configured");
+ }
+
+ return await _clientWrapper.GetGameMetadataAsync(gameType);
+ }
+
+ [McpServerTool]
+ [Description("Gets a human-readable description of a specific game type including rules and how to play.")]
+ public async Task GetGameDescriptionAsync(string gameType)
+ {
+ if (_clientWrapper == null)
+ {
+ throw new InvalidOperationException("Client wrapper is not configured");
+ }
+
+ return await _clientWrapper.GetGameDescriptionAsync(gameType);
+ }
+
+ [McpServerTool]
+ [Description("Gets information about the currently authenticated user. Returns user data as JSON.")]
+ public async Task GetCurrentUserAsync()
+ {
+ if (_clientWrapper == null)
+ {
+ throw new InvalidOperationException("Client wrapper is not configured");
+ }
+
+ return await _clientWrapper.GetCurrentUserAsync();
+ }
+
+ [McpServerTool]
+ [Description("Gets metadata about message types used in the Deckster system. Returns message metadata as JSON.")]
+ public async Task GetMessageMetadataAsync()
+ {
+ if (_clientWrapper == null)
+ {
+ throw new InvalidOperationException("Client wrapper is not configured");
+ }
+
+ return await _clientWrapper.GetMessageMetadataAsync();
+ }
+}
diff --git a/src/Deckster.McpServer/Wrappers/DecksterClientWrapper.cs b/src/Deckster.McpServer/Wrappers/DecksterClientWrapper.cs
new file mode 100644
index 00000000..99c16a10
--- /dev/null
+++ b/src/Deckster.McpServer/Wrappers/DecksterClientWrapper.cs
@@ -0,0 +1,254 @@
+using System.Net.Http.Json;
+using System.Text.Json;
+using Deckster.Client;
+using Deckster.Client.Games.CrazyEights;
+using Deckster.McpServer.Configuration;
+
+namespace Deckster.McpServer.Wrappers;
+
+public class DecksterClientWrapper
+{
+ private readonly DecksterClient _client;
+ private readonly HttpClient _httpClient;
+ private readonly string _baseUrl;
+
+ public DecksterClientWrapper(DecksterClient client, HttpClient httpClient, string baseUrl)
+ {
+ _client = client ?? throw new ArgumentNullException(nameof(client));
+ _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
+ _baseUrl = baseUrl ?? throw new ArgumentNullException(nameof(baseUrl));
+ }
+
+ public static async Task CreateAsync(DecksterConfiguration configuration)
+ {
+ ArgumentNullException.ThrowIfNull(configuration);
+
+ var client = await DecksterClient.LogInOrRegisterAsync(
+ configuration.ServerUrl,
+ configuration.Username,
+ configuration.Password);
+
+ var httpClient = new HttpClient();
+
+ return new DecksterClientWrapper(client, httpClient, configuration.ServerUrl);
+ }
+
+ public async Task CreateGameAsync(string gameType, string gameName)
+ {
+ ArgumentNullException.ThrowIfNull(gameType);
+ ArgumentNullException.ThrowIfNull(gameName);
+
+ var gameInfo = gameType switch
+ {
+ "CrazyEights" => await _client.CrazyEights().CreateAsync(gameName),
+ _ => throw new ArgumentException($"Unknown game type: {gameType}", nameof(gameType))
+ };
+
+ return gameInfo.Id;
+ }
+
+ public async Task AddBotAsync(string gameType, string gameId)
+ {
+ ArgumentNullException.ThrowIfNull(gameType);
+ ArgumentNullException.ThrowIfNull(gameId);
+
+ switch (gameType)
+ {
+ case "CrazyEights":
+ await _client.CrazyEights().AddBotAsync(gameId);
+ break;
+ default:
+ throw new ArgumentException($"Unknown game type: {gameType}", nameof(gameType));
+ }
+ }
+
+ public async Task StartGameAsync(string gameType, string gameId)
+ {
+ ArgumentNullException.ThrowIfNull(gameType);
+ ArgumentNullException.ThrowIfNull(gameId);
+
+ switch (gameType)
+ {
+ case "CrazyEights":
+ await _client.CrazyEights().StartGameAsync(gameId);
+ break;
+ default:
+ throw new ArgumentException($"Unknown game type: {gameType}", nameof(gameType));
+ }
+ }
+
+ public async Task GetPreviousGamesAsync(string gameType)
+ {
+ ArgumentNullException.ThrowIfNull(gameType);
+
+ var gameTypeLower = gameType.ToLowerInvariant();
+ var url = $"{_baseUrl}/{gameTypeLower}/previousgames";
+
+ var response = await _httpClient.GetAsync(url);
+ response.EnsureSuccessStatusCode();
+
+ var json = await response.Content.ReadAsStringAsync();
+
+ // Pretty-print JSON
+ var jsonDoc = JsonDocument.Parse(json);
+ return JsonSerializer.Serialize(jsonDoc, new JsonSerializerOptions { WriteIndented = true });
+ }
+
+ public async Task GetActiveGamesAsync(string gameType)
+ {
+ ArgumentNullException.ThrowIfNull(gameType);
+
+ var gameTypeLower = gameType.ToLowerInvariant();
+ var url = $"{_baseUrl}/{gameTypeLower}/games";
+
+ var response = await _httpClient.GetAsync(url);
+ response.EnsureSuccessStatusCode();
+
+ var json = await response.Content.ReadAsStringAsync();
+
+ // Pretty-print JSON
+ var jsonDoc = JsonDocument.Parse(json);
+ return JsonSerializer.Serialize(jsonDoc, new JsonSerializerOptions { WriteIndented = true });
+ }
+
+ public async Task GetGameStateAsync(string gameType, string gameName)
+ {
+ ArgumentNullException.ThrowIfNull(gameType);
+ ArgumentNullException.ThrowIfNull(gameName);
+
+ var gameTypeLower = gameType.ToLowerInvariant();
+ var url = $"{_baseUrl}/{gameTypeLower}/games/{gameName}";
+
+ var response = await _httpClient.GetAsync(url);
+ response.EnsureSuccessStatusCode();
+
+ var json = await response.Content.ReadAsStringAsync();
+
+ // Pretty-print JSON
+ var jsonDoc = JsonDocument.Parse(json);
+ return JsonSerializer.Serialize(jsonDoc, new JsonSerializerOptions { WriteIndented = true });
+ }
+
+ public async Task DeleteGameAsync(string gameType, string gameName)
+ {
+ ArgumentNullException.ThrowIfNull(gameType);
+ ArgumentNullException.ThrowIfNull(gameName);
+
+ var gameTypeLower = gameType.ToLowerInvariant();
+ var url = $"{_baseUrl}/{gameTypeLower}/games/{gameName}";
+
+ var response = await _httpClient.DeleteAsync(url);
+ response.EnsureSuccessStatusCode();
+ }
+
+ public async Task GetGameOverviewAsync(string gameType)
+ {
+ ArgumentNullException.ThrowIfNull(gameType);
+
+ var gameTypeLower = gameType.ToLowerInvariant();
+ var url = $"{_baseUrl}/{gameTypeLower}";
+
+ var response = await _httpClient.GetAsync(url);
+ response.EnsureSuccessStatusCode();
+
+ var json = await response.Content.ReadAsStringAsync();
+
+ // Pretty-print JSON
+ var jsonDoc = JsonDocument.Parse(json);
+ return JsonSerializer.Serialize(jsonDoc, new JsonSerializerOptions { WriteIndented = true });
+ }
+
+ public async Task GetHistoricalGameAsync(string gameType, string gameId)
+ {
+ ArgumentNullException.ThrowIfNull(gameType);
+ ArgumentNullException.ThrowIfNull(gameId);
+
+ var gameTypeLower = gameType.ToLowerInvariant();
+ var url = $"{_baseUrl}/{gameTypeLower}/previousgames/{gameId}";
+
+ var response = await _httpClient.GetAsync(url);
+ response.EnsureSuccessStatusCode();
+
+ var json = await response.Content.ReadAsStringAsync();
+
+ // Pretty-print JSON
+ var jsonDoc = JsonDocument.Parse(json);
+ return JsonSerializer.Serialize(jsonDoc, new JsonSerializerOptions { WriteIndented = true });
+ }
+
+ public async Task GetHistoricalGameVersionAsync(string gameType, string gameId, int version)
+ {
+ ArgumentNullException.ThrowIfNull(gameType);
+ ArgumentNullException.ThrowIfNull(gameId);
+
+ var gameTypeLower = gameType.ToLowerInvariant();
+ var url = $"{_baseUrl}/{gameTypeLower}/previousgames/{gameId}/{version}";
+
+ var response = await _httpClient.GetAsync(url);
+ response.EnsureSuccessStatusCode();
+
+ var json = await response.Content.ReadAsStringAsync();
+
+ // Pretty-print JSON
+ var jsonDoc = JsonDocument.Parse(json);
+ return JsonSerializer.Serialize(jsonDoc, new JsonSerializerOptions { WriteIndented = true });
+ }
+
+ public async Task GetGameMetadataAsync(string gameType)
+ {
+ ArgumentNullException.ThrowIfNull(gameType);
+
+ var gameTypeLower = gameType.ToLowerInvariant();
+ var url = $"{_baseUrl}/{gameTypeLower}/metadata";
+
+ var response = await _httpClient.GetAsync(url);
+ response.EnsureSuccessStatusCode();
+
+ var json = await response.Content.ReadAsStringAsync();
+
+ // Pretty-print JSON
+ var jsonDoc = JsonDocument.Parse(json);
+ return JsonSerializer.Serialize(jsonDoc, new JsonSerializerOptions { WriteIndented = true });
+ }
+
+ public async Task GetGameDescriptionAsync(string gameType)
+ {
+ ArgumentNullException.ThrowIfNull(gameType);
+
+ var gameTypeLower = gameType.ToLowerInvariant();
+ var url = $"{_baseUrl}/{gameTypeLower}/description";
+
+ var response = await _httpClient.GetAsync(url);
+ response.EnsureSuccessStatusCode();
+
+ return await response.Content.ReadAsStringAsync();
+ }
+
+ public async Task GetCurrentUserAsync()
+ {
+ var url = $"{_baseUrl}/me";
+
+ var response = await _httpClient.GetAsync(url);
+ response.EnsureSuccessStatusCode();
+
+ var json = await response.Content.ReadAsStringAsync();
+
+ // Pretty-print JSON
+ var jsonDoc = JsonDocument.Parse(json);
+ return JsonSerializer.Serialize(jsonDoc, new JsonSerializerOptions { WriteIndented = true });
+ }
+
+ public async Task GetMessageMetadataAsync()
+ {
+ var url = $"{_baseUrl}/meta/messages";
+
+ var response = await _httpClient.GetAsync(url);
+ response.EnsureSuccessStatusCode();
+
+ var json = await response.Content.ReadAsStringAsync();
+
+ // Pretty-print JSON
+ var jsonDoc = JsonDocument.Parse(json);
+ return JsonSerializer.Serialize(jsonDoc, new JsonSerializerOptions { WriteIndented = true });
+ }
+}
diff --git a/src/Deckster.sln b/src/Deckster.sln
index 7cb1e793..54ff2afc 100644
--- a/src/Deckster.sln
+++ b/src/Deckster.sln
@@ -49,6 +49,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "99. Other Clients", "99. Ot
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Deckster.CardsAgainstHumanity.SampleClient", "Deckster.CardsAgainstHumanity.SampleClient\Deckster.CardsAgainstHumanity.SampleClient.csproj", "{D61DB3A9-F1A7-4DF4-8DAB-880F3551E005}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "69. MCP", "69. MCP", "{E2B58A61-452A-4B58-93DA-4DDB77BD9206}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Deckster.McpServer", "Deckster.McpServer\Deckster.McpServer.csproj", "{FD8A525D-0E39-43CB-ACB4-9E6692D6BEAE}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Deckster.McpServer.Tests", "Deckster.McpServer.Tests\Deckster.McpServer.Tests.csproj", "{2708C9B6-4F39-4095-8716-01203B41137E}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -123,6 +129,14 @@ Global
{D61DB3A9-F1A7-4DF4-8DAB-880F3551E005}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D61DB3A9-F1A7-4DF4-8DAB-880F3551E005}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D61DB3A9-F1A7-4DF4-8DAB-880F3551E005}.Release|Any CPU.Build.0 = Release|Any CPU
+ {FD8A525D-0E39-43CB-ACB4-9E6692D6BEAE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FD8A525D-0E39-43CB-ACB4-9E6692D6BEAE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FD8A525D-0E39-43CB-ACB4-9E6692D6BEAE}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FD8A525D-0E39-43CB-ACB4-9E6692D6BEAE}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2708C9B6-4F39-4095-8716-01203B41137E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2708C9B6-4F39-4095-8716-01203B41137E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2708C9B6-4F39-4095-8716-01203B41137E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2708C9B6-4F39-4095-8716-01203B41137E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -145,6 +159,8 @@ Global
{15FD6712-AFF7-42EC-BBAD-28E23B77107B} = {E66EE07A-1B20-4842-A77D-5A71F8FD3E0B}
{2D7A1F3D-43D3-4030-B5FF-B2D53089F1CF} = {2AB8FFE7-41B8-45D8-8CE7-B3275ED3A0A2}
{D61DB3A9-F1A7-4DF4-8DAB-880F3551E005} = {E66EE07A-1B20-4842-A77D-5A71F8FD3E0B}
+ {FD8A525D-0E39-43CB-ACB4-9E6692D6BEAE} = {E2B58A61-452A-4B58-93DA-4DDB77BD9206}
+ {2708C9B6-4F39-4095-8716-01203B41137E} = {E2B58A61-452A-4B58-93DA-4DDB77BD9206}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {AE622423-92CC-4E60-ADDA-E76DEFF6CB96}