⚠️ !! IMPORTANT !!
This project is just a little experiment to make some experience with the semantic kernel and agents. It is not for production use! At the current step it's just a prototype and a playground for myself.
Scope. This document explains what each component in the solution does, how the components interact (runtime and build‑time), and which contracts tie them together (APIs, streaming formats, data model). It also calls out notable implementation details, configuration, and gaps/TODOs discovered while reviewing the code.
| Project | Type | Purpose / Responsibilities | Key Dependencies |
|---|---|---|---|
| JW.TA.Domain | .NET class library | Domain model (aggregates, entities, value objects, enums) and basic invariants | EF Core attributes (data annotations) |
| JW.TA.Infrastructure | .NET class library | EF Core DbContext and persistence plumbing | EF Core 9.0.8, SQL Server provider |
| JW.TA.Application | .NET class library | Application layer for slash‑command (CLI) parsing & routing; contains handlers that query the DbContext | Infrastructure (DbContext), LINQ/Regex |
| JW.TA.Agents | .NET class library | LLM orchestration using Microsoft Semantic Kernel Agents; implements IGameOrchestrator (single‑turn, streaming) |
Microsoft.SemanticKernel.* (1.64.x), ModelContextProtocol |
| JW.TA.Api | ASP.NET Core Web API | Public HTTP API: chat streaming endpoint (NDJSON), world CRUD for MCP tools, dev seed, AOAI debug checks; DI composition root | ASP.NET Core, EF Core, SK, IMcpClient (MCP) |
| JW.TA.MCPServer | Python (FastMCP) | MCP (Model Context Protocol) server exposing tools for dice/combat and persistence proxies that call the Web API; foundations for NDJSON streaming relay | fastmcp, httpx, aiohttp, uvicorn |
| JW.TA.MCPTools | Azure Functions (C# isolated) | Placeholder Azure Functions host (currently a “Hello” function) intended as a future MCP tools ingress/egress point | Azure Functions Worker v4 |
| JW.TA.Client.Web | Angular (esproj) | Frontend (Angular) workspace; not included in detail here but CORS hints show local dev on port 51681 | Angular CLI, Karma/Jasmine |
| JW.TA.Seed | Console app | Placeholder for seed/maintenance scripts | — |
| JW.TA.Contracts | .NET class library | Contracts placeholder (currently empty) | — |
flowchart LR
UI["Angular Frontend\nJW.TA.Client.Web"] -- "NDJSON POST /api/chat/stream" --> API["ASP.NET Core API\nJW.TA.Api"]
subgraph "API Services"
API -- "Routes to" --> ChatCtrl["ChatController\n/api/chat/stream"]
API -- "Routes to" --> WorldCtrl["WorldController\n/api/world/*"]
API -- "EF Core" --> DB["SQL Server"]
ChatCtrl -- "DI" --> Orchestrator["IGameOrchestrator\n(SkGroupChatOrchestrator – JW.TA.Agents)"]
Orchestrator -- "AOAI Chat" --> AOAI["Azure OpenAI\nChat Completions"]
Orchestrator -. "optional: Tools via" .-> MCPClient["IMcpClient\n(Model Context Protocol)"]
end
subgraph "MCP (Python FastMCP)"
MCPClient -- "HTTP /mcp" --> MCPSrv["FastMCP Server\nJW.TA.MCPServer"]
MCPSrv --> ToolDice["MCP Tool: roll_dice / resolve_combat"]
MCPSrv --> ToolDb["MCP Tool: db_upsert_entity / db_query"]
ToolDb -- "REST" --> WorldCtrl
end
MCPSrv -. "kann NDJSON weiterleiten" .-> ChatCtrl
subgraph "Azure Functions (optional/future)"
AF["Azure Functions\nJW.TA.MCPTools"]:::faded
end
classDef faded fill:#eee,stroke:#ccc,color:#999
sequenceDiagram
participant UI as Angular UI
participant API as ASP.NET API (ChatController)
participant ORCH as Orchestrator (SK Agents)
participant AOAI as Azure OpenAI
participant MCPc as IMcpClient
participant MCPS as FastMCP Server
participant TOOL as MCP Tool (z.B. db_upsert_entity)
participant WORLD as WorldController
participant DB as SQL Server
UI->>API: POST /api/chat/stream {"message":"..."}
alt CLI Command
API->>API: Parse & Execute CLI
API-->>UI: NDJSON {"type":"system"...} + {"type":"done"}
else Narrative mit Tool
API->>ORCH: RunTurnAsync()
ORCH->>AOAI: ChatCompletion (Prompt/Turn)
AOAI-->>ORCH: Tool-Call angefordert
ORCH->>MCPc: Invoke tool (IMcpClient)
MCPc->>MCPS: HTTP /mcp
MCPS->>TOOL: Execute tool
opt Proxy zur WebAPI/DB
TOOL->>WORLD: POST /api/world/*
WORLD->>DB: SQL
DB-->>WORLD: OK
WORLD-->>TOOL: IDs/Data
end
TOOL-->>MCPS: Tool-Result
MCPS-->>MCPc: Result
MCPc-->>ORCH: Result
ORCH->>AOAI: Fortsetzen mit Tool-Result
AOAI-->>ORCH: Finaler Text
ORCH-->>API: Token-Stream
API-->>UI: NDJSON {"type":"token"...} ... {"type":"done"}
end
Narrative (runtime):
- Frontend calls
POST /api/chat/streamwith the player message. - ChatController (API) first tries to parse slash‑commands. If it’s a CLI command, the router executes a handler and immediately streams a system block as NDJSON.
If it’s regular text, the controller invokes
IGameOrchestratorwhich streams the GM’s reply as NDJSON tokens. - The orchestrator (SK Agents) talks to Azure OpenAI and returns a single, concise reply (single‑turn policy).
- WorldController exposes minimal CRUD/search endpoints used by external tools (like the MCP server) to persist generated content.
- The Python FastMCP server exposes MCP tools (
db_upsert_entity,db_query,roll_dice,resolve_combat) and proxies persistence to the Web API.
Purpose. Clean domain model with invariants and audit fields.
Key types
- BaseEntity / AggregateRoot:
Id,CreatedAtUtc,UpdatedAtUtc,[Timestamp] RowVersion, domain events queue (inAggregateRoot). - Entities:
Character,Item,InventoryItem(junction),Location,Monster,Quest,Encounter,CombatRound,Faction,RuleSet,GameEvent. - Enums:
ItemType,Rarity,LocationType,QuestStatus,EncounterType,EncounterState. - Guard utilities for invariants (e.g.,
NotNullOrWhiteSpace,Min,NotNegative).
Notable design choices
- JSON blobs for flexible attributes: many entities carry
*Jsonstrings (AttributesJson,StatsJson,LootTableJson,RewardJson, etc.) to keep schema flexible for LLM‑driven content. - Audit/concurrency baked into base types.
Interacts with
- Infrastructure mapping via EF Core.
- Application handlers query via DbContext.
⚠️ Attention:InventoryItemhas no explicit key property. EF Core will require a key configuration (composite(CharacterId, ItemId)) viaOnModelCreating. No configuration files are shown; you will need one.
Purpose. EF Core integration.
GameDbContext
- Declares
DbSet<>for all domain aggregates and junctions. - Applies configurations from assembly (placeholder hook).
- Overrides
SaveChanges/Asyncto apply audit timestamps via EF’s Property API; this respects the protected setters on base entities.
Interacts with
- API registers DbContext (SQL Server).
- Application handlers and controllers query and persist entities.
Purpose. Slash‑command parsing, routing, and execution with minimal, structured responses for the UI to render as “system” blocks.
Key flow
-
Parser (
CliCommandParser): Detects lines starting with/, tokenizes (supports"quoted values"and--flag value), normalizes toCliCommand(Type, Subcommand, Flags, Args, Raw). -
Router (
CliCommandRouter): Finds the firstICliCommandHandlerthat supports the command and runs it. -
Handlers:
Help: prints supported commands.Inventory: lists items for--character <id>.Skills: reads and printsAttributesJsonfor a character.Map:show|listbasic locations (optional--type,--top).Save: appends aGameEventSavePointwith a lightweight snapshot.Login/Logout: MVP stubs (real auth deferred to UI/API auth).
-
Response format (
CliResponse):Title,Lines, optionalFooter. The API streams this as a single NDJSON “system” object.
Interacts with
- API
ChatControlleruses parser and router to short‑circuit chat turns for CLI commands. - DbContext for data retrieval and
SavePointevents.
Purpose. Encapsulate GM turn orchestration using Semantic Kernel Agents with a single‑turn policy.
Key interfaces & classes
-
IGameOrchestrator→RunTurnAsync(string, CancellationToken)returns token stream (IAsyncEnumerable<string>). -
Managers to enforce “one GM reply per user message”:
-
SingleReplyRoundRobinManager(custom):ShouldRequestUserInput→false(single turn).ShouldTerminate→ true once a non‑user message appears after the last user message.FilterResults→ returns the last non‑user message after last user message.
-
-
SkGroupChatOrchestrator:
- Builds SK
Kernel(injected from API). - Creates GM agent, and optionally WorldBuilder + CombatAgent for trio mode.
- Uses
GroupChatOrchestration+ the custom manager above. - Streams the final text by chunking to small tokens (32 chars) for NDJSON (
Chunk()helper). - Options (
AgentsOptions):TeamMode: "Single"|"Trio",MaxTurns(1 for Single, 2–3 for Trio).
- Builds SK
-
StubGmOrchestrator: deterministic fallback if AOAI not configured (simply echoes
"GM: " + message"in small chunks).
Interacts with
- API DI resolves
IGameOrchestratortoSkGroupChatOrchestrator. - Azure OpenAI chat completions via SK provider.
- (Planned) MCP tools (IMcpClient registered in API for future use by agents).
ℹ️ GM prompt enforces German output for the narrative itself:
"Sprich Deutsch. Du bist der Spielleiter (GM): Antworte kurz ...". The API/UI metadata is still English.
Purpose. Public HTTP surface for the frontend and for tool integrations.
Composition root (Program.cs)
-
JSON options: enums as strings, camelCase, ignore nulls.
-
Config sources:
appsettings.local.json(optional), environment variables. -
Binds AgentsOptions from
Agentssection. -
Registers
IMcpClientusing SSE transport (ModelContextProtocol) pointing toMcp:BaseUrl. This is provisioned for future tool usage by agents or controllers. -
Registers Azure OpenAI chat via SK:
AOAI:Endpoint(must not contain/openaiper guard),AOAI:Deployment,AOAI:ApiKey.
-
Registers IGameOrchestrator as singleton
SkGroupChatOrchestrator. -
CORS for Angular dev on port 51681.
-
Response compression enabled.
-
DbContext (SQL Server) with migrations assembly in Infrastructure.
-
Registers CLI services (router + handlers).
-
Swagger in development.
Controllers
-
ChatController (
POST /api/chat/stream):-
Accepts:
{ "message": "<text>" }. -
If the line is a CLI command (starts with
/), parses and executes handler:- streams one NDJSON line:
{"type":"system","data":{ "title":..., "lines":..., "footer":... }}, then{"type":"done"}.
- streams one NDJSON line:
-
Else invokes orchestrator, streaming lines: multiple
{"type":"token","data":"..."}and finally{"type":"done"}. -
Error cases stream
{"type":"error","data":"..."}followed by{"type":"done"}.
-
-
WorldController (minimal persistence endpoints used by MCP tools):
POST /api/world/monster{name, level, stats?, loot?}→{ id }POST /api/world/monster/search{ q: { name? }, top? }→[ { id, name, level }, ... ]POST /api/world/item{name, type, rarity, props?}→{ id }(validates enums)POST /api/world/item/searchsimilarly
-
DevController
POST /api/dev/seed/basiccreates a sample character, items, location, and an audit event. Returns resource IDs for quick testing.
-
DebugController
GET /api/debug/aoai/deployments(lists AOAI deployments by raw REST).GET /api/debug/aoai/ping(asks the model to respond with “ok”).-
⚠️ Uses an undefinedhttpvariable. InjectHttpClientvia DI: add a constructor parameterHttpClient httpor[FromServices] HttpClient httpin the action.
Interacts with
- Frontend (CORS).
- Agents (GM orchestration).
- DbContext (persistence).
- MCP (client registered; not yet used in controllers).
Purpose. A Model Context Protocol server offering tools to LLMs/agents. It also includes NDJSON streaming relay utilities for upstream sources.
Key parts
-
FastMCP app created via
create_streamable_http_app(mcp, "/mcp", stateless_http=True). -
Tools
roll_dice(formula, seed?)— deterministic NdM±K roller.resolve_combat(attack, defense, seed)— MVP “to-hit + damage” resolver.db_upsert_entity(kind, payload)— proxy toPOST /api/world/{kind}on the Web API.db_query(kind, q, top?)— proxy toPOST /api/world/{kind}/search.
-
NDJSON relay (
_stream_ndjson) Utility to open an upstream NDJSON HTTP stream and forward each line to the MCP client viactx.report_progress(...). Accepts only standards‑compliant headers (avoids SSE specifics), with safety limits (max line size) and tolerant fallbacks if the upstream replies with JSON once.Currently this utility isn’t wired into a specific
@mcp.tool, but it’s ready to be used if/when you want to expose a “proxy stream” tool to relayChatControlleroutput. -
Headers & middleware A
StandardsOnlyMiddlewaresetsCache-Control: no-cache, no-transformand keeps buffering off for proxies.
Interacts with
- Web API
WorldControllerfor persistence/search. - (Future) Web API
ChatControllerif you expose a stream relay tool.
Configuration
API_BASE_URL = "http://localhost:5000"(adjust to the API base address).- Uses
.envif present (local‑only). - Run locally:
python mcp_server.py(uvicorn on127.0.0.1:8080).
Purpose. Placeholder Functions app named “MCP Tools”. It currently exposes only Function1 (“Welcome to Azure Functions!”).
Intended role
- A future surface to host serverless MCP tools, or to receive Azure‑originating events/actions that must be fed into the MCP ecosystem (and possibly relayed upstream as NDJSON).
Purpose. Browser UI (Angular). The repo includes test scaffolding (karma.conf.js). CORS in the API allows local dev on http(s)://localhost:51681 and 127.0.0.1:51681.
Expected behavior
-
POST to
/api/chat/streamand consume NDJSON:{"type":"token","data":"..."}{"type":"system","data":{ "title": string, "lines": string[], "footer": string? }}{"type":"error","data":"..."}{"type":"done"}
A simple NDJSON reader splits on \n and JSON‑parses each line.
Endpoint: POST /api/chat/stream
Request:
{ "message": "text from player (or /command ...)" }Response (content type: application/x-ndjson; charset=utf-8): a sequence of newline‑delimited JSON objects:
-
LLM tokens (normal chat):
{"type":"token","data":"partial text"} ... {"type":"done"} -
CLI command:
{"type":"system","data":{"title":"Inventory","lines":["3x Heiltrank (Common)","1x Eisen-Schwert (Rare)"],"footer":"Total items: 2"}} {"type":"done"} -
Error:
{"type":"error","data":"GM failed: ..."} {"type":"done"}
Notes
- GM output is chunked into ~32‑char tokens by the orchestrator for a smoother stream.
- The custom group‑chat manager ensures exactly one assistant reply per user message.
Within the MCP server, tools use HTTP to call the Web API:
db_upsert_entity("monster", {...})→POST /api/world/monsterdb_query("item", {"name":"Heil"}, 10)→POST /api/world/item/search
Result shape is always { "status": <http status>, "data": <parsed json or null> }.
classDiagram
class Character {
+Guid Id
+string Name
+int Level
+string AttributesJson
+decimal Gold
+Guid? FactionId
}
class Item {
+Guid Id
+string Name
+ItemType Type
+Rarity Rarity
+string PropertiesJson
}
class InventoryItem {
+Guid CharacterId
+Guid ItemId
+int Quantity
}
class Location {
+Guid Id
+string Name
+LocationType Type
+string Region
+string Description
+Guid? ParentLocationId
}
class Monster {
+Guid Id
+string Name
+int Level
+string StatsJson
+string LootTableJson
}
class Quest {
+Guid Id
+string Title
+string Description
+QuestStatus Status
+Guid? AssignedToCharacterId
+Guid? GivenByNpcId
+string RewardJson
}
class Encounter {
+Guid Id
+EncounterType Type
+EncounterState State
+int Seed
+Guid? LocationId
}
class CombatRound {
+Guid Id
+Guid EncounterId
+int RoundNumber
+string LogJson
}
class Faction {
+Guid Id
+string Name
+string Description
}
class RuleSet {
+Guid Id
+string Name
+string Version
+bool IsActive
+string RulesJson
}
class GameEvent {
+Guid Id
+string Type
+string PayloadJson
+string? ActorType
+Guid? ActorId
+string? CorrelationId
}
Character "1" -- "many" InventoryItem
Item "1" -- "many" InventoryItem
Location "0..1" <-- "many" Location : Parent
Encounter "1" <-- "many" CombatRound
EF Core mapping note: add a composite key for
InventoryItem:modelBuilder.Entity<InventoryItem>() .HasKey(ii => new { ii.CharacterId, ii.ItemId });
sequenceDiagram
participant UI as Angular UI
participant API as ChatController
participant CLI as Cli Router/Handlers
participant GM as IGameOrchestrator
participant AOAI as Azure OpenAI (via SK)
UI->>API: POST /api/chat/stream {"message":"/inventory --character <id>"}
API->>CLI: TryParse + Route
CLI-->>API: CliResponse (Title/Lines)
API-->>UI: {"type":"system", data:{...}}\n{"type":"done"}
UI->>API: POST /api/chat/stream {"message":"I open the tavern door."}
API->>GM: RunTurnAsync("I open the tavern door.")
GM->>AOAI: ChatCompletion (SK Agents/GroupChat)
AOAI-->>GM: "Short German narration..."
GM-->>API: async stream of small tokens
API-->>UI: {"type":"token","data":"..."} ... {"type":"done"}
sequenceDiagram
participant Agent as LLM/Agent (future)
participant MCP as FastMCP Server
participant API as WorldController
participant DB as SQL
Agent->>MCP: mcp.call(db_upsert_entity, "monster", {...})
MCP->>API: POST /api/world/monster
API->>DB: INSERT Monster
DB-->>API: new Id
API-->>MCP: { id: ... }
MCP-->>Agent: { status:200, data:{ id: ... } }
-
Azure OpenAI (required for real GM):
AOAI:Endpoint(e.g.,https://<name>.openai.azure.com, do not include/openai)AOAI:Deployment(model deployment name)AOAI:ApiKey
-
MCP client:
Mcp:BaseUrl(e.g.,http://127.0.0.1:8080/mcp)
-
Database:
SqlConnectionString(SQL Server)
-
Agents:
Agents:TeamMode="Single"or"Trio"Agents:MaxTurns=1..n
Sample appsettings.local.json
{
"AOAI": {
"Endpoint": "https://<resource>.openai.azure.com",
"Deployment": "gpt-5-mini",
"ApiKey": "..."
},
"MCP": {
"BaseUrl": "http://127.0.0.1:8080/mcp"
},
"Agents": {
"TeamMode": "Trio",
"MaxTurns": 5
},
"SqlConnectionString": "Server=.;Database=JW_TextAdventure;Trusted_Connection=True;TrustServerCertificate=True"
}API_BASE_URLinmcp_server.py→ Web API base (defaulthttp://localhost:5000).
- Currently no configuration required beyond standard Functions settings.
-
Prereqs
- .NET 8 SDK, SQL Server (local), Node.js (Angular), Python 3.13 (for MCP).
-
Database
- Ensure
SqlConnectionStringpoints to a reachable SQL Server. - First run will apply migrations (see migration scope fix above).
- Ensure
-
Run the API
cd JW.TA.Api→dotnet run(defaults tohttp://localhost:5000)
-
Run the MCP server (optional but useful for tool tests)
cd JW.TA.MCPServer→ create venv,pip install -r requirements.txt→python mcp_server.py- Check it listens on
127.0.0.1:8080/mcp
-
Seed minimal data
POST http://localhost:5000/api/dev/seed/basic
-
Test chat (cURL)
curl -N -H "Content-Type: application/json" \ -d '{ "message": "/map list --top 5" }' \ http://localhost:5000/api/chat/stream
You should see NDJSON lines including a
systemblock. -
Run Angular
- Use your Angular dev server on port 51681 (per CORS). Implement an NDJSON reader that updates the UI progressively.
-
Add a CLI command
- Extend
CliCommandType. - Implement
ICliCommandHandler(CanHandleandExecuteAsync). - Register the handler in DI (Program.cs).
- Extend
-
Add a world entity
- Add a domain aggregate and EF mapping.
- Create endpoints in
WorldController(create + search). - Add MCP tools in Python to proxy to these endpoints.
-
Change GM team behavior
- Adjust
AgentsOptions(TeamMode,MaxTurns). - Extend
SkGroupChatOrchestratorwith specialized agents or per‑agent kernels.
- Adjust
-
Consume MCP tools from .NET
IMcpClientis already registered; wire it into the orchestrator or dedicated controllers to call MCP tools from C#.
-
Audit trail:
GameEventis append‑only. CLI actions and seed logic record events. -
Streaming hygiene:
- HTTP response headers prevent buffering (
no-transform) and set NDJSON content type. - The Python relay mirrors these headers if you choose to proxy streams via MCP.
- HTTP response headers prevent buffering (
-
Compression: Gzip enabled for HTTP responses (useful for non‑streaming responses).
-
Swagger: Available in Development for quick inspection of controllers.
-
EF mapping for
InventoryItemAdd composite key and relationships (else EF will complain about missing key). -
DebugController HttpClient Inject
HttpClient(undefinedhttpvariable). -
Agents (minor compile issues)
- In
SkGroupChatOrchestrator.RunTurnAsync,linkedCtsis referenced but not defined. Create a linked CTS from the methodctor remove. - Ensure all necessary
usingdirectives and package references match the preview packages used (Agents.Orchestration,Runtime.InProcess).
- In
-
Azure Functions project Currently a stub; wire to actual tools or remove until needed.
-
Contracts project Empty; remove or populate with shared DTOs if you intend to use it between services.
- Body:
{ "message": string } - Return: NDJSON lines (
token|system|error|done)
POST /api/world/monster→{ id }POST /api/world/monster/search→[ { id, name, level } ]POST /api/world/item→{ id }(validatestype,rarity)POST /api/world/item/search→[ { id, name, type, rarity } ]
POST /api/dev/seed/basic→ IDs for created sample contentGET /api/debug/aoai/deployments→ raw AOAI deployments JSONGET /api/debug/aoai/ping→{ ok: bool, text? | error?, message? }
- .NET 8.0
- EF Core 9.0.8 (SQL Server)
- Semantic Kernel 1.64.0 (+ Agents preview packages)
- ModelContextProtocol 0.3.0‑preview.4
- Azure Functions Worker v4 (isolated, .NET 8)
- Python 3.13 (FastMCP, aiohttp, httpx, uvicorn)
- Angular (workspace scaffold; dev server CORS on port 51681)
- The solution cleanly separates Domain, Application (CLI), Infrastructure (EF), Agents (GM orchestration), API, MCP server (tools/proxy), and Frontend concerns.
- Chat flow is NDJSON streaming with an early CLI capture path.
- Agents are designed for single‑turn, deterministic handoff via a custom GroupChat manager with optional team expansion.
- MCP provides a bridge for procedural tools and persistence, enabling future agent/tool composition.
- A small set of fixes (EF key, migration scope, HttpClient injection, a minor orchestrator variable) will make the current code production‑ready for local testing.
If you want, I can add developer‑facing README snippets (copy‑paste ready) for the root and each subproject, or provide a minimal Angular NDJSON service to plug into your UI.