Skip to content

Commit d48ad44

Browse files
committed
Added new MCP server for Azure. Set up initial file structure for Claude
1 parent c25af96 commit d48ad44

12 files changed

Lines changed: 612 additions & 0 deletions

File tree

.github/workflows/main.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ on:
99
env:
1010
AZURE_WEBAPP_NAME: XenobiasoftSudoku-prod
1111
AZURE_API_NAME: XenobiasoftSudokuApi-prod
12+
AZURE_MCP_NAME: XenobiasoftSudokuMcp-prod
1213
AZURE_SWA_NAME: swa-sudoku-xenobiasoft-prod
1314
CUSTOM_DOMAIN_NAME: "sudoku.xenobiasoft.com"
1415
DOTNET_VERSION: "10.x"
@@ -55,6 +56,9 @@ jobs:
5556
- name: Publish API
5657
run: dotnet publish src/backend/Sudoku.Api/Sudoku.Api.csproj --configuration Release --output ${{ github.workspace }}/publish-api --no-restore --no-build
5758

59+
- name: Publish MCP Server
60+
run: dotnet publish src/backend/Sudoku.McpServer/Sudoku.McpServer.csproj --configuration Release --output ${{ github.workspace }}/publish-mcp --no-restore --no-build
61+
5862
- name: Upload publish-web artifact
5963
uses: actions/upload-artifact@v4
6064
with:
@@ -67,6 +71,12 @@ jobs:
6771
name: publish-api
6872
path: ${{ github.workspace }}/publish-api
6973

74+
- name: Upload publish-mcp artifact
75+
uses: actions/upload-artifact@v4
76+
with:
77+
name: publish-mcp
78+
path: ${{ github.workspace }}/publish-mcp
79+
7080
build-frontend:
7181
runs-on: ubuntu-latest
7282

@@ -267,6 +277,18 @@ jobs:
267277
app-name: ${{ env.AZURE_API_NAME }}
268278
package: ${{ github.workspace }}/publish-api
269279

280+
- name: Download publish-mcp artifact
281+
uses: actions/download-artifact@v4
282+
with:
283+
name: publish-mcp
284+
path: ${{ github.workspace }}/publish-mcp
285+
286+
- name: Deploy MCP Server
287+
uses: azure/webapps-deploy@v3
288+
with:
289+
app-name: ${{ env.AZURE_MCP_NAME }}
290+
package: ${{ github.workspace }}/publish-mcp
291+
270292
deploy-swa:
271293
needs: [deploy-infra, build-frontend]
272294
runs-on: ubuntu-latest

CLAUDE.md

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Commands
6+
7+
### Build & Run
8+
```bashV
9+
# Build entire solution
10+
dotnet build
11+
12+
# Run Blazor Server UI
13+
dotnet run --project src/backend/Sudoku.Blazor
14+
15+
# Run REST API
16+
dotnet run --project src/backend/Sudoku.Api
17+
18+
# Run React frontend (requires API running separately)
19+
cd src/frontend/Sudoku.React
20+
npm install
21+
npm run dev
22+
```
23+
24+
### Testing
25+
```bash
26+
# Run all tests
27+
dotnet test
28+
29+
# Run a single test class
30+
dotnet test --filter "FullyQualifiedName~SudokuGameTests"
31+
32+
# Run a single test method
33+
dotnet test --filter "FullyQualifiedName~SudokuGameTests.MakeMoveValidMoveRaisesEvent"
34+
35+
# Watch mode
36+
dotnet watch test
37+
38+
# React tests
39+
npm run test # run once
40+
npm run test:watch # watch mode
41+
npm run test:coverage # with coverage
42+
```
43+
44+
### Linting
45+
```bash
46+
npm run lint # React frontend only
47+
```
48+
49+
## Architecture
50+
51+
This solution follows **Clean Architecture** with **DDD** and **CQRS** across five backend projects and two frontend projects.
52+
53+
### Layer Dependency Flow
54+
```
55+
Presentation (API, Blazor, React)
56+
57+
Application (CQRS: Commands, Queries, Handlers)
58+
59+
Domain (Entities, Value Objects, Domain Events)
60+
61+
Infrastructure (Repositories, Azure Services, Event Dispatching)
62+
```
63+
64+
Outer layers depend on inner layers only. No business logic lives in controllers or handlers — all invariants are enforced inside domain aggregates.
65+
66+
### Key Aggregates
67+
- **SudokuGame** — The primary aggregate root. Owns game state transitions via methods like `MakeMove()`, `StartGame()`, `PauseGame()`, `ResumeGame()`. Raises domain events on state changes.
68+
- **SudokuPuzzle** — Represents the puzzle grid. Contains a collection of `Cell` value objects and handles validation.
69+
- **Cell** — Value object representing a single cell: value, fixed status, possible values.
70+
71+
### CQRS via MediatR
72+
All application operations flow through `IMediator`. There are 14 commands (e.g., `CreateGame`, `MakeMove`, `UndoLastMove`, `ResetGame`) and 4 queries (e.g., `GetGame`, `GetPlayerGames`, `ValidateGame`). All handlers return `Result<T>` for unified error handling — never throw exceptions for expected failures.
73+
74+
### Domain Events
75+
Domain aggregates raise events (e.g., `GameCreatedEvent`, `MoveMadeEvent`, `GameCompletedEvent`) with no knowledge of handlers. `IDomainEventDispatcher` in Infrastructure dispatches them after persistence.
76+
77+
### Repositories
78+
Interfaces are defined in the **Application** layer; implementations live in **Infrastructure**:
79+
- `CosmosDbGameRepository` — primary persistent store
80+
- `AzureBlobGameRepository` — legacy store (being migrated away)
81+
- `InMemoryPuzzleRepository` — puzzle generation only (performance optimization, not persisted)
82+
83+
### Testing Patterns
84+
Tests use a container-based DI mocking framework (`DepenMock`). Test classes inherit from `BaseTestByAbstraction` or `BaseTestByType`. Resolve the SUT via `ResolveSut()` and mocks via `Container.ResolveMock<T>()`. Use AutoFixture for test data, FluentAssertions for assertions. Blazor components are tested with bunit. CI enforces **80% line coverage**.
85+
86+
### CI/CD
87+
GitHub Actions (`.github/workflows/main.yml`) builds, tests (with coverage threshold enforcement), and deploys to Azure on every merge to `main`. Coverage reports are posted as PR annotations.

Sudoku.sln

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GitHub", "GitHub", "{7DBF77
1313
EndProject
1414
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sudoku.Api", "src\backend\Sudoku.Api\Sudoku.Api.csproj", "{825D9287-9613-8670-E0F8-77BF8584E423}"
1515
EndProject
16+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sudoku.McpServer", "src\backend\Sudoku.McpServer\Sudoku.McpServer.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}"
17+
EndProject
1618
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sudoku.AppHost", "src\backend\Sudoku.AppHost\Sudoku.AppHost.csproj", "{9EB926AC-F796-4872-B081-568F8F3D7CFE}"
1719
EndProject
1820
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sudoku.ServiceDefaults", "src\backend\Sudoku.ServiceDefaults\Sudoku.ServiceDefaults.csproj", "{2B5F2794-B11C-7F6D-BA4E-3FD968E80C96}"
@@ -45,6 +47,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "modules", "modules", "{F2DF
4547
infra\modules\monitoring.bicep = infra\modules\monitoring.bicep
4648
infra\modules\ssl.bicep = infra\modules\ssl.bicep
4749
infra\modules\staticwebapp.bicep = infra\modules\staticwebapp.bicep
50+
infra\modules\mcp.bicep = infra\modules\mcp.bicep
4851
infra\modules\storage.bicep = infra\modules\storage.bicep
4952
EndProjectSection
5053
EndProject
@@ -88,6 +91,18 @@ Global
8891
{825D9287-9613-8670-E0F8-77BF8584E423}.Release|x64.Build.0 = Release|Any CPU
8992
{825D9287-9613-8670-E0F8-77BF8584E423}.Release|x86.ActiveCfg = Release|Any CPU
9093
{825D9287-9613-8670-E0F8-77BF8584E423}.Release|x86.Build.0 = Release|Any CPU
94+
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
95+
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU
96+
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x64.ActiveCfg = Debug|Any CPU
97+
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x64.Build.0 = Debug|Any CPU
98+
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x86.ActiveCfg = Debug|Any CPU
99+
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x86.Build.0 = Debug|Any CPU
100+
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU
101+
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU
102+
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x64.ActiveCfg = Release|Any CPU
103+
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x64.Build.0 = Release|Any CPU
104+
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x86.ActiveCfg = Release|Any CPU
105+
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x86.Build.0 = Release|Any CPU
91106
{9EB926AC-F796-4872-B081-568F8F3D7CFE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
92107
{9EB926AC-F796-4872-B081-568F8F3D7CFE}.Debug|Any CPU.Build.0 = Debug|Any CPU
93108
{9EB926AC-F796-4872-B081-568F8F3D7CFE}.Debug|x64.ActiveCfg = Debug|Any CPU

infra/main.bicep

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ param webAppName string
2323
@description('Name of the API app.')
2424
param apiAppName string
2525

26+
@description('Name of the MCP server app.')
27+
param mcpAppName string
28+
2629
@description('App Service Plan SKU.')
2730
param appServicePlanSku string = 'B1'
2831

@@ -150,6 +153,20 @@ module compute 'modules/compute.bicep' = {
150153
}
151154
}
152155

156+
module mcp 'modules/mcp.bicep' = {
157+
name: 'mcp'
158+
params: {
159+
location: location
160+
environment: environment
161+
appServicePlanName: appServicePlanName
162+
mcpAppName: mcpAppName
163+
logAnalyticsWorkspaceId: monitoring.outputs.logAnalyticsWorkspaceId
164+
logAnalyticsWorkspaceCustomerId: monitoring.outputs.logAnalyticsWorkspaceCustomerId
165+
appInsightsConnectionString: monitoring.outputs.appInsightsConnectionString
166+
keyVaultUri: keyvault.outputs.keyVaultUri
167+
}
168+
}
169+
153170
module staticwebapp 'modules/staticwebapp.bicep' = {
154171
name: 'staticwebapp'
155172
params: {
@@ -193,6 +210,7 @@ output staticWebAppUrl string = staticwebapp.outputs.staticWebAppUrl
193210
output resourceGroupName string = resourceGroup().name
194211
output webAppUrl string = compute.outputs.webAppUrl
195212
output apiAppUrl string = compute.outputs.apiAppUrl
213+
output mcpAppUrl string = mcp.outputs.mcpAppUrl
196214
output appInsightsConnectionString string = monitoring.outputs.appInsightsConnectionString
197215
output cosmosDbEndpoint string = storage.outputs.cosmosDbEndpoint
198216
output keyVaultUri string = keyvault.outputs.keyVaultUri

infra/modules/mcp.bicep

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
// ---------------------------------------------------------------------------
2+
// MCP Server App Service
3+
//
4+
// Hosts the Model Context Protocol server that exposes Application Insights
5+
// analytics tools to AI agents (e.g. Claude Desktop).
6+
//
7+
// Auth model
8+
// Outbound — SystemAssigned Managed Identity → Azure Monitor / Log Analytics
9+
// Inbound — Enable Easy Auth (Azure AD) via the portal or a separate Bicep
10+
// module once you have an Entra ID app registration.
11+
// ---------------------------------------------------------------------------
12+
13+
param location string
14+
param environment string
15+
param appServicePlanName string
16+
param mcpAppName string
17+
18+
@description('Log Analytics workspace ARM resource ID (for the role assignment scope).')
19+
param logAnalyticsWorkspaceId string
20+
21+
@description('Log Analytics workspace GUID (customerId) — passed to the app as AppInsights__WorkspaceId.')
22+
param logAnalyticsWorkspaceCustomerId string
23+
24+
param appInsightsConnectionString string
25+
param keyVaultUri string
26+
27+
var tags = {
28+
environment: environment
29+
project: 'XenobiaSoftSudoku'
30+
component: 'mcp'
31+
}
32+
33+
// ---------------------------------------------------------------------------
34+
// Reference the shared App Service Plan (read-only)
35+
// ---------------------------------------------------------------------------
36+
37+
resource appServicePlan 'Microsoft.Web/serverfarms@2023-12-01' existing = {
38+
name: appServicePlanName
39+
}
40+
41+
// ---------------------------------------------------------------------------
42+
// MCP App Service
43+
// ---------------------------------------------------------------------------
44+
45+
resource mcpApp 'Microsoft.Web/sites@2023-12-01' = {
46+
name: mcpAppName
47+
location: location
48+
kind: 'app,linux'
49+
tags: tags
50+
identity: {
51+
type: 'SystemAssigned'
52+
}
53+
properties: {
54+
serverFarmId: appServicePlan.id
55+
httpsOnly: true
56+
clientAffinityEnabled: false
57+
reserved: true
58+
keyVaultReferenceIdentity: 'SystemAssigned'
59+
siteConfig: {
60+
linuxFxVersion: 'DOTNETCORE|10.0'
61+
alwaysOn: true
62+
http20Enabled: true
63+
numberOfWorkers: 1
64+
}
65+
}
66+
}
67+
68+
resource mcpAppConfig 'Microsoft.Web/sites/config@2023-12-01' = {
69+
parent: mcpApp
70+
name: 'web'
71+
properties: {
72+
linuxFxVersion: 'DOTNETCORE|10.0'
73+
alwaysOn: true
74+
http20Enabled: true
75+
minTlsVersion: '1.2'
76+
scmMinTlsVersion: '1.2'
77+
ftpsState: 'FtpsOnly'
78+
healthCheckPath: '/health-check'
79+
managedPipelineMode: 'Integrated'
80+
loadBalancing: 'LeastRequests'
81+
virtualApplications: [
82+
{
83+
virtualPath: '/'
84+
physicalPath: 'site\\wwwroot'
85+
preloadEnabled: true
86+
}
87+
]
88+
remoteDebuggingEnabled: false
89+
webSocketsEnabled: true // required for SSE keep-alive
90+
use32BitWorkerProcess: true
91+
}
92+
}
93+
94+
resource mcpAppSettings 'Microsoft.Web/sites/config@2023-12-01' = {
95+
parent: mcpApp
96+
name: 'appsettings'
97+
properties: {
98+
APPLICATIONINSIGHTS_CONNECTION_STRING: appInsightsConnectionString
99+
ApplicationInsightsAgent_EXTENSION_VERSION: '~3'
100+
ConnectionStrings__AzureKeyVault: keyVaultUri
101+
// Workspace GUID consumed by LogsQueryClient in ApplicationInsightsTools
102+
AppInsights__WorkspaceId: logAnalyticsWorkspaceCustomerId
103+
}
104+
}
105+
106+
// ---------------------------------------------------------------------------
107+
// RBAC — grant Managed Identity read access to Log Analytics
108+
//
109+
// Log Analytics Reader (73c42c96-874c-492b-b04d-ab87d138a893)
110+
// Allows QueryWorkspace calls against the workspace.
111+
//
112+
// Monitoring Reader (43d0d8ad-25c7-4714-9337-8ba259a9fe05)
113+
// Allows reading metrics from the Application Insights resource.
114+
// ---------------------------------------------------------------------------
115+
116+
var logAnalyticsReaderRoleId = '73c42c96-874c-492b-b04d-ab87d138a893'
117+
var monitoringReaderRoleId = '43d0d8ad-25c7-4714-9337-8ba259a9fe05'
118+
119+
resource logAnalyticsReaderAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
120+
// Scope to the workspace so the identity can only query this workspace
121+
scope: resourceGroup()
122+
name: guid(logAnalyticsWorkspaceId, mcpApp.id, logAnalyticsReaderRoleId)
123+
properties: {
124+
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', logAnalyticsReaderRoleId)
125+
principalId: mcpApp.identity.principalId
126+
principalType: 'ServicePrincipal'
127+
}
128+
}
129+
130+
resource monitoringReaderAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
131+
scope: resourceGroup()
132+
name: guid(logAnalyticsWorkspaceId, mcpApp.id, monitoringReaderRoleId)
133+
properties: {
134+
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', monitoringReaderRoleId)
135+
principalId: mcpApp.identity.principalId
136+
principalType: 'ServicePrincipal'
137+
}
138+
}
139+
140+
// ---------------------------------------------------------------------------
141+
// Outputs
142+
// ---------------------------------------------------------------------------
143+
144+
output mcpAppUrl string = mcpApp.properties.defaultHostName
145+
output mcpAppId string = mcpApp.id
146+
output mcpAppPrincipalId string = mcpApp.identity.principalId

infra/modules/monitoring.bicep

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ resource failureAnomaliesAlert 'microsoft.alertsmanagement/smartdetectoralertrul
9999
}
100100

101101
output logAnalyticsWorkspaceId string = logAnalyticsWorkspace.id
102+
// customerId is the workspace GUID used by LogsQueryClient.QueryWorkspaceAsync
103+
output logAnalyticsWorkspaceCustomerId string = logAnalyticsWorkspace.properties.customerId
102104
output appInsightsId string = appInsights.id
103105
output appInsightsConnectionString string = appInsights.properties.ConnectionString
104106
output appInsightsInstrumentationKey string = appInsights.properties.InstrumentationKey

infra/params/prod.bicepparam

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ param environment = 'prod'
88
param appServicePlanName = 'XenobiasoftServicePlan-prod'
99
param webAppName = 'XenobiasoftSudoku-prod'
1010
param apiAppName = 'XenobiasoftSudokuApi-prod'
11+
param mcpAppName = 'XenobiasoftSudokuMcp-prod'
1112
param appServicePlanSku = 'B1'
1213
param customDomainName = 'sudoku.xenobiasoft.com'
1314
param enableCustomDomain = true

0 commit comments

Comments
 (0)