Context: The JWT and MCP parts are real code fixes regardless of environment. The Docker-daemon connection is a doctrine violation (below), not merely a dev artifact.
Problem — three unauthenticated control planes:
The API connects to an unauthenticated Docker daemon on tcp://…:2375 — Docker API = root on the host. This violates api-conventions.md: "The API has NO Docker socket and must never get one — cluster topology reads come from the Redis snapshot via IClusterTopologyService."
GameServer WebSocket trusts unsigned/forged JWTs (reads sub with no signature/issuer/audience/expiry check) → full account impersonation.
MCP server is unauthenticated and stamps a caller-supplied userId → read/write any user's data.
Fix: Remove the API's IDockerClient/DockerSwarmService and read topology from the Redis snapshot; validate GameServer JWTs against Keycloak JWKS (signature + issuer + audience + lifetime); require auth on /mcp and derive the acting user from the authenticated principal.
Acceptance criteria: API has no Docker client; GameServer rejects forged tokens; /mcp requires auth and ignores client-supplied user IDs.
Effort: M · Related: #401 (SignalR hubs — separate anonymous-hub gap).
**Context:** The JWT and MCP parts are real code fixes regardless of environment. The Docker-daemon connection is a doctrine violation (below), not merely a dev artifact.
**Problem — three unauthenticated control planes:**
1. The API connects to an unauthenticated Docker daemon on `tcp://…:2375` — Docker API = root on the host. This violates `api-conventions.md`: *"The API has NO Docker socket and must never get one — cluster topology reads come from the Redis snapshot via `IClusterTopologyService`."*
2. GameServer WebSocket trusts unsigned/forged JWTs (reads `sub` with no signature/issuer/audience/expiry check) → full account impersonation.
3. MCP server is unauthenticated and stamps a caller-supplied `userId` → read/write any user's data.
**Evidence:**
- `SpikerSoft.Api/Extensions/ServiceCollectionExtensions.cs:1021-1082` (Docker `tcp://…:2375`)
- `SpikerSoft.GameServer/Network/WebSocketHandler.cs:49-58`
- `SpikerSoft.AI.MCPServer/Program.cs:79`; `CalendarTools.cs` → `AuthenticatedSpikerSoftApiClient.cs:44-48`
**Fix:** Remove the API's `IDockerClient`/`DockerSwarmService` and read topology from the Redis snapshot; validate GameServer JWTs against Keycloak JWKS (signature + issuer + audience + lifetime); require auth on `/mcp` and derive the acting user from the authenticated principal.
**Acceptance criteria:** API has no Docker client; GameServer rejects forged tokens; `/mcp` requires auth and ignores client-supplied user IDs.
**Effort:** M · Related: #401 (SignalR hubs — separate anonymous-hub gap).
spikerj
added the agentic label 2026-07-05 20:24:36 +00:00
Server-side JWKS validation on the game WebSocket; forged/expired/wrong-issuer/wrong-audience/unsigned tokens now resolve to unauthenticated instead of an impersonated sub. 10 unit tests cover every rejection path. Flagged for a live-Keycloak smoke test before merge (positive path isn't headless-verifiable; config mirrors the API's proven Authentication:* block, so low risk).
Item 1 (API → Docker 2375) — scoped; needs frontend-contract verification, not a blind read-swap
Dug into it so the next person starts with eyes open:
DockerSwarmController is read-only — 4 GETs (services, services/{id}, daemon, summary), all via GetDockerSwarmServicesQuery. No management/writes, so removing the Docker client doesn't drop any control-plane feature. Good.
But it's not a clean swap to IClusterTopologyService.GetDockerSwarmServicesQueryHandler sources from IDockerSwarmService (the tcp://…:2375 client) and returns the rich DockerSwarmServicesResponseDto/DockerSwarmServiceDto shape. IClusterTopologyService.GetTopologyAsync() returns a different abstraction (ClusterTopologyDto: Nodes/Workloads/Instances), and there's no topology equivalent for the daemon endpoint.
Therefore the real work is: (a) confirm which DockerSwarmServiceDto fields the Angular cluster views actually consume; (b) confirm the docker-monitor Redis snapshot carries them (may need extending the worker's snapshot); (c) rewrite the handler to map ClusterWorkloadDto[] → DockerSwarmServiceDto[] + decide what daemon/summary return from a snapshot; (d) then delete the IDockerClient/DockerSwarmService registration (ServiceCollectionExtensions.cs:1021-1082).
I'm not blind-shipping (c)/(d): a wrong mapping silently returns partial/empty data to the cluster dashboard, and I can't verify the frontend contract or snapshot coverage headless. Happy to implement it once we confirm the consumed fields + snapshot coverage (or if we're fine dropping/synthesizing the daemon view). Item 3 (MCP /mcp auth) still open.
### Item 2 (GameServer JWT) shipped — backend PR #158 (in review)
Server-side JWKS validation on the game WebSocket; forged/expired/wrong-issuer/wrong-audience/unsigned tokens now resolve to unauthenticated instead of an impersonated `sub`. 10 unit tests cover every rejection path. Flagged for a live-Keycloak smoke test before merge (positive path isn't headless-verifiable; config mirrors the API's proven `Authentication:*` block, so low risk).
### Item 1 (API → Docker 2375) — scoped; needs frontend-contract verification, not a blind read-swap
Dug into it so the next person starts with eyes open:
- **`DockerSwarmController` is read-only** — 4 GETs (`services`, `services/{id}`, `daemon`, `summary`), all via `GetDockerSwarmServicesQuery`. No management/writes, so removing the Docker client doesn't drop any control-plane feature. Good.
- **But it's not a clean swap to `IClusterTopologyService`.** `GetDockerSwarmServicesQueryHandler` sources from `IDockerSwarmService` (the `tcp://…:2375` client) and returns the rich `DockerSwarmServicesResponseDto`/`DockerSwarmServiceDto` shape. `IClusterTopologyService.GetTopologyAsync()` returns a *different* abstraction (`ClusterTopologyDto`: `Nodes`/`Workloads`/`Instances`), and there's **no topology equivalent for the `daemon` endpoint**.
- **Therefore the real work is:** (a) confirm which `DockerSwarmServiceDto` fields the Angular cluster views actually consume; (b) confirm the docker-monitor Redis snapshot carries them (may need extending the worker's snapshot); (c) rewrite the handler to map `ClusterWorkloadDto[] → DockerSwarmServiceDto[]` + decide what `daemon`/`summary` return from a snapshot; (d) then delete the `IDockerClient`/`DockerSwarmService` registration (`ServiceCollectionExtensions.cs:1021-1082`).
I'm not blind-shipping (c)/(d): a wrong mapping silently returns partial/empty data to the cluster dashboard, and I can't verify the frontend contract or snapshot coverage headless. Happy to implement it once we confirm the consumed fields + snapshot coverage (or if we're fine dropping/synthesizing the `daemon` view). **Item 3** (MCP `/mcp` auth) still open.
Item 2 (GameServer JWT forgery) — merged to master via PR #158 (commit f68ba51).
The game WebSocket now derives userId only from a token that cryptographically validates against the Keycloak JWKS (signature + issuer + audience + lifetime); forged/expired/wrong-party/unsigned tokens resolve to unauthenticated (spectator) instead of an impersonated sub. Validation runs once at connect (never on the game loop) and is local crypto against cached keys; a startup pre-warm (JwksPrewarmService) ensures even the first connect never waits on a key fetch. 12 unit tests.
Remaining on this epic:
Item 1 (API → Docker 2375) — scoped in the comment above; needs the frontend-contract check + possible docker-monitor snapshot extension before the read-path can be safely moved off the Docker client. Not a blind swap.
Item 3 (MCP /mcp auth + stop trusting caller-supplied userId) — still open; needs to establish who calls /mcp today (adding auth could break the MCP client integration if it doesn't present a Keycloak token).
Keeping open for 1 & 3.
**Item 2 (GameServer JWT forgery) — merged to `master`** via PR #158 (commit `f68ba51`).
The game WebSocket now derives `userId` only from a token that cryptographically validates against the Keycloak JWKS (signature + issuer + audience + lifetime); forged/expired/wrong-party/unsigned tokens resolve to unauthenticated (spectator) instead of an impersonated `sub`. Validation runs once at connect (never on the game loop) and is local crypto against cached keys; a startup pre-warm (`JwksPrewarmService`) ensures even the first connect never waits on a key fetch. 12 unit tests.
**Remaining on this epic:**
- **Item 1** (API → Docker `2375`) — scoped in the comment above; needs the frontend-contract check + possible docker-monitor snapshot extension before the read-path can be safely moved off the Docker client. Not a blind swap.
- **Item 3** (MCP `/mcp` auth + stop trusting caller-supplied `userId`) — still open; needs to establish who calls `/mcp` today (adding auth could break the MCP client integration if it doesn't present a Keycloak token).
Keeping open for 1 & 3.
Item 3 (MCP auth) — scoped; blocked on one external-integration decision
Verified the shape so the decision is teed up:
SpikerSoft.AI.MCPServer is an HTTP-hosted MCP server — AddMcpServer().WithHttpTransport() + app.MapMcp("/mcp") (ModelContextProtocol.AspNetCore 1.3.0), health at /api/healthz. No UseAuthentication/UseAuthorization, no [Authorize] on /mcp.
It's reached by external AI-assistant clients, not internal services — the only in-repo reference is docker-compose.yml (deployment); nothing in the codebase calls /mcp.
Tools authenticate to the SpikerSoft API via a service account (SpikerSoftAuthenticationService → AuthenticatedSpikerSoftApiClient), and act on a caller-supplied userId — that's the impersonation lever: whoever reaches /mcp can pass any user id.
Why it can't be blind-shipped: the two halves are coupled — deriving the acting user from an authenticated principal (fixing the caller-supplied userId) requires first authenticating /mcp, and turning on auth breaks every MCP client that doesn't present a token. ModelContextProtocol.AspNetCore supports OAuth/bearer for HTTP transport, so the real question is a deployment one:
How should MCP clients authenticate? (a) per-user OAuth (each client acts as its own Keycloak user → derive userId from the token, drop the caller-supplied one — the correct end state); (b) a shared bearer/service credential on /mcp (closes anonymous access but keeps caller-supplied userId — partial); (c) network-restrict /mcp (no public exposure) as an interim.
I can implement (a) or (b) on a call, but it needs the MCP-client auth model decided first (and a smoke test that the configured clients still connect). Flagging rather than guessing.
### Item 3 (MCP auth) — scoped; blocked on one external-integration decision
Verified the shape so the decision is teed up:
- `SpikerSoft.AI.MCPServer` is an **HTTP-hosted** MCP server — `AddMcpServer().WithHttpTransport()` + `app.MapMcp("/mcp")` (`ModelContextProtocol.AspNetCore` 1.3.0), health at `/api/healthz`. **No `UseAuthentication`/`UseAuthorization`, no `[Authorize]` on `/mcp`.**
- It's reached by **external AI-assistant clients**, not internal services — the only in-repo reference is `docker-compose.yml` (deployment); nothing in the codebase calls `/mcp`.
- Tools authenticate to the SpikerSoft API via a **service account** (`SpikerSoftAuthenticationService` → `AuthenticatedSpikerSoftApiClient`), and act on a **caller-supplied `userId`** — that's the impersonation lever: whoever reaches `/mcp` can pass any user id.
**Why it can't be blind-shipped:** the two halves are coupled — deriving the acting user from an authenticated principal (fixing the caller-supplied `userId`) requires first authenticating `/mcp`, and turning on auth **breaks every MCP client that doesn't present a token**. `ModelContextProtocol.AspNetCore` supports OAuth/bearer for HTTP transport, so the real question is a deployment one:
> **How should MCP clients authenticate?** (a) per-user OAuth (each client acts as its own Keycloak user → derive `userId` from the token, drop the caller-supplied one — the correct end state); (b) a shared bearer/service credential on `/mcp` (closes anonymous access but keeps caller-supplied `userId` — partial); (c) network-restrict `/mcp` (no public exposure) as an interim.
I can implement (a) or (b) on a call, but it needs the MCP-client auth model decided first (and a smoke test that the configured clients still connect). Flagging rather than guessing.
Resolved the way this ticket asks (remove, not re-point) after confirming the earlier "needs frontend-contract verification" blocker was empty:
/api/DockerSwarm/* is read-only and consumed by nobody — Angular calls it 0 times (the live cluster dashboard runs on ClusterHub + /api/Cluster, fed by the docker-monitor Redis snapshot), and no MCP tool calls it (server has only CalendarTools; the generated client's methods are dead).
Removed DockerSwarmController, its query/handler, IDockerSwarmService/DockerSwarmService (the client that connected to unauthenticated tcp://…:2375 with 2375/2376/host.docker.internal fallbacks + a blocking startup probe), the IDockerClient DI wiring, and the now-dead Docker.DotNet ref in SpikerSoft.Business.
The DockerMonitor worker keeps its own Docker client (the one service meant to talk to Docker, publishing the snapshot). DTOs kept in SpikerSoft.Data (inert; still referenced by the generated MCP client). Full Tests.Unit build clean; 83 Cluster tests pass.
The API tier no longer has any Docker-daemon dependency — matching the .claude/rules/api-conventions.md invariant ("the API has NO Docker socket and must never get one").
Epic status: item 1 ✅ (#162), item 2 ✅ (#158, GameServer JWT). Item 3 (MCP /mcp auth + caller-supplied userId) remains open — blocked on the MCP-client auth-model decision (per-user OAuth vs shared bearer vs network-restrict). Keeping this open for item 3.
**Item 1 (API → Docker 2375) — done, merged via PR #162 (`0811a2f`).**
Resolved the way this ticket asks (remove, not re-point) after confirming the earlier "needs frontend-contract verification" blocker was empty:
- `/api/DockerSwarm/*` is read-only and consumed by **nobody** — Angular calls it 0 times (the live cluster dashboard runs on `ClusterHub` + `/api/Cluster`, fed by the docker-monitor Redis snapshot), and no MCP tool calls it (server has only `CalendarTools`; the generated client's methods are dead).
- Removed `DockerSwarmController`, its query/handler, `IDockerSwarmService`/`DockerSwarmService` (the client that connected to unauthenticated `tcp://…:2375` with `2375/2376/host.docker.internal` fallbacks + a blocking startup probe), the `IDockerClient` DI wiring, and the now-dead `Docker.DotNet` ref in `SpikerSoft.Business`.
- The **DockerMonitor worker keeps its own Docker client** (the one service meant to talk to Docker, publishing the snapshot). DTOs kept in `SpikerSoft.Data` (inert; still referenced by the generated MCP client). Full `Tests.Unit` build clean; 83 Cluster tests pass.
The API tier no longer has any Docker-daemon dependency — matching the `.claude/rules/api-conventions.md` invariant ("the API has NO Docker socket and must never get one").
**Epic status:** item 1 ✅ (#162), item 2 ✅ (#158, GameServer JWT). **Item 3** (MCP `/mcp` auth + caller-supplied `userId`) remains open — blocked on the MCP-client auth-model decision (per-user OAuth vs shared bearer vs network-restrict). Keeping this open for item 3.
Two of the three control planes are closed; please confirm the third.
Docker 2375 — done. grep -rn '2375\|DockerSwarmController\|DockerClientConfiguration' SpikerSoft.Api returns nothing on master; spikersoft-backend#162 removed the legacy /api/DockerSwarm control plane and the API's Docker client. Cluster topology now comes from the Redis snapshot, per api-conventions.md.
GameServer JWT — done. SpikerSoft.GameServer/Network/GameServerTokenValidator.cs validates properly now (JwtSecurityTokenHandler with MapInboundClaims = false, TokenValidationParameters with ValidateIssuerSigningKey = true at :60-72). No more trusting an unsigned sub.
MCP server — still unresolved as far as I can tell.SpikerSoft.AI.MCPServer has authentication for its outbound calls to the API (ISpikerSoftAuthenticationService, bearer token in AuthenticatedSpikerSoftApiClient), but I found no inbound authentication on the MCP server itself, and Program.cs:79 maps /api/healthz anonymously. #563 (MCPServer pre-deployment cleanup) is the natural home for it if you'd rather move it there.
Worth splitting so items 1 and 2 stop carrying an open ticket.
— 2026-08-06 tracker sweep, Opus 5 Agent. Staying open.
**Two of the three control planes are closed; please confirm the third.**
1. **Docker 2375** — done. `grep -rn '2375\|DockerSwarmController\|DockerClientConfiguration' SpikerSoft.Api` returns nothing on master; spikersoft-backend#162 removed the legacy `/api/DockerSwarm` control plane and the API's Docker client. Cluster topology now comes from the Redis snapshot, per `api-conventions.md`.
2. **GameServer JWT** — done. `SpikerSoft.GameServer/Network/GameServerTokenValidator.cs` validates properly now (`JwtSecurityTokenHandler` with `MapInboundClaims = false`, `TokenValidationParameters` with `ValidateIssuerSigningKey = true` at :60-72). No more trusting an unsigned `sub`.
3. **MCP server** — **still unresolved as far as I can tell.** `SpikerSoft.AI.MCPServer` has authentication for its *outbound* calls to the API (`ISpikerSoftAuthenticationService`, bearer token in `AuthenticatedSpikerSoftApiClient`), but I found no inbound authentication on the MCP server itself, and `Program.cs:79` maps `/api/healthz` anonymously. #563 (MCPServer pre-deployment cleanup) is the natural home for it if you'd rather move it there.
Worth splitting so items 1 and 2 stop carrying an open ticket.
— 2026-08-06 tracker sweep, Opus 5 Agent. Staying open.
Migrated to spikerj/spikersoft-backend#574 as part of the umbrella-tracker breakup — narrowed to item 3, since items 1 and 2 are verified done.
Verified 2026-08-07 against spikersoft-backend@98102023:
Docker 2375 — done. No DockerSwarmController, DockerClientConfiguration or 2375 under SpikerSoft.Api; cluster topology reads the Redis snapshot per api-conventions.md. Shipped by PR #162.
GameServer JWT — done.SpikerSoft.GameServer/Network/GameServerTokenValidator.cs uses JwtSecurityTokenHandler with MapInboundClaims = false and TokenValidationParameters with ValidateIssuerSigningKey = true. Shipped by PR #158.
MCP server — not done.SpikerSoft.AI.MCPServer/Program.cs still has noUseAuthentication/UseAuthorization; /api/healthz is mapped anonymously at :79 and /mcp is a bare app.MapMcp("/mcp") at :81 with no RequireAuthorization().
One correction worth recording: the item-3 exposure is latent, not live. The MCP server is not deployed anywhere — no swarm service, no stack file in spikersoft-infrastructure, mcp.spikersoft.com does not resolve, and the only deployment reference is a commented-out block at docker-compose.yml:52-61. So it is a pre-deployment blocker, and the "how should MCP clients authenticate" decision is the same decision as how it gets deployed.
Closing here. Work now lives in the repo that holds the fix, so fixes #574 in a PR will auto-close it on merge. The umbrella tracker keeps cross-repo epics only.
— Opus 5 Agent
Migrated to **spikerj/spikersoft-backend#574** as part of the umbrella-tracker breakup — narrowed to item 3, since items 1 and 2 are verified done.
Verified 2026-08-07 against `spikersoft-backend@98102023`:
1. **Docker 2375 — done.** No `DockerSwarmController`, `DockerClientConfiguration` or `2375` under `SpikerSoft.Api`; cluster topology reads the Redis snapshot per `api-conventions.md`. Shipped by PR #162.
2. **GameServer JWT — done.** `SpikerSoft.GameServer/Network/GameServerTokenValidator.cs` uses `JwtSecurityTokenHandler` with `MapInboundClaims = false` and `TokenValidationParameters` with `ValidateIssuerSigningKey = true`. Shipped by PR #158.
3. **MCP server — not done.** `SpikerSoft.AI.MCPServer/Program.cs` still has **no** `UseAuthentication`/`UseAuthorization`; `/api/healthz` is mapped anonymously at :79 and `/mcp` is a bare `app.MapMcp("/mcp")` at :81 with no `RequireAuthorization()`.
One correction worth recording: the item-3 exposure is **latent, not live**. The MCP server is not deployed anywhere — no swarm service, no stack file in `spikersoft-infrastructure`, `mcp.spikersoft.com` does not resolve, and the only deployment reference is a commented-out block at `docker-compose.yml:52-61`. So it is a pre-deployment blocker, and the "how should MCP clients authenticate" decision is the same decision as how it gets deployed.
Closing here. Work now lives in the repo that holds the fix, so `fixes #574` in a PR will auto-close it on merge. The umbrella tracker keeps cross-repo epics only.
— Opus 5 Agent
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Context: The JWT and MCP parts are real code fixes regardless of environment. The Docker-daemon connection is a doctrine violation (below), not merely a dev artifact.
Problem — three unauthenticated control planes:
tcp://…:2375— Docker API = root on the host. This violatesapi-conventions.md: "The API has NO Docker socket and must never get one — cluster topology reads come from the Redis snapshot viaIClusterTopologyService."subwith no signature/issuer/audience/expiry check) → full account impersonation.userId→ read/write any user's data.Evidence:
SpikerSoft.Api/Extensions/ServiceCollectionExtensions.cs:1021-1082(Dockertcp://…:2375)SpikerSoft.GameServer/Network/WebSocketHandler.cs:49-58SpikerSoft.AI.MCPServer/Program.cs:79;CalendarTools.cs→AuthenticatedSpikerSoftApiClient.cs:44-48Fix: Remove the API's
IDockerClient/DockerSwarmServiceand read topology from the Redis snapshot; validate GameServer JWTs against Keycloak JWKS (signature + issuer + audience + lifetime); require auth on/mcpand derive the acting user from the authenticated principal.Acceptance criteria: API has no Docker client; GameServer rejects forged tokens;
/mcprequires auth and ignores client-supplied user IDs.Effort: M · Related: #401 (SignalR hubs — separate anonymous-hub gap).
Item 2 (GameServer JWT) shipped — backend PR #158 (in review)
Server-side JWKS validation on the game WebSocket; forged/expired/wrong-issuer/wrong-audience/unsigned tokens now resolve to unauthenticated instead of an impersonated
sub. 10 unit tests cover every rejection path. Flagged for a live-Keycloak smoke test before merge (positive path isn't headless-verifiable; config mirrors the API's provenAuthentication:*block, so low risk).Item 1 (API → Docker 2375) — scoped; needs frontend-contract verification, not a blind read-swap
Dug into it so the next person starts with eyes open:
DockerSwarmControlleris read-only — 4 GETs (services,services/{id},daemon,summary), all viaGetDockerSwarmServicesQuery. No management/writes, so removing the Docker client doesn't drop any control-plane feature. Good.IClusterTopologyService.GetDockerSwarmServicesQueryHandlersources fromIDockerSwarmService(thetcp://…:2375client) and returns the richDockerSwarmServicesResponseDto/DockerSwarmServiceDtoshape.IClusterTopologyService.GetTopologyAsync()returns a different abstraction (ClusterTopologyDto:Nodes/Workloads/Instances), and there's no topology equivalent for thedaemonendpoint.DockerSwarmServiceDtofields the Angular cluster views actually consume; (b) confirm the docker-monitor Redis snapshot carries them (may need extending the worker's snapshot); (c) rewrite the handler to mapClusterWorkloadDto[] → DockerSwarmServiceDto[]+ decide whatdaemon/summaryreturn from a snapshot; (d) then delete theIDockerClient/DockerSwarmServiceregistration (ServiceCollectionExtensions.cs:1021-1082).I'm not blind-shipping (c)/(d): a wrong mapping silently returns partial/empty data to the cluster dashboard, and I can't verify the frontend contract or snapshot coverage headless. Happy to implement it once we confirm the consumed fields + snapshot coverage (or if we're fine dropping/synthesizing the
daemonview). Item 3 (MCP/mcpauth) still open.Item 2 (GameServer JWT forgery) — merged to
mastervia PR #158 (commitf68ba51).The game WebSocket now derives
userIdonly from a token that cryptographically validates against the Keycloak JWKS (signature + issuer + audience + lifetime); forged/expired/wrong-party/unsigned tokens resolve to unauthenticated (spectator) instead of an impersonatedsub. Validation runs once at connect (never on the game loop) and is local crypto against cached keys; a startup pre-warm (JwksPrewarmService) ensures even the first connect never waits on a key fetch. 12 unit tests.Remaining on this epic:
2375) — scoped in the comment above; needs the frontend-contract check + possible docker-monitor snapshot extension before the read-path can be safely moved off the Docker client. Not a blind swap./mcpauth + stop trusting caller-supplieduserId) — still open; needs to establish who calls/mcptoday (adding auth could break the MCP client integration if it doesn't present a Keycloak token).Keeping open for 1 & 3.
Item 3 (MCP auth) — scoped; blocked on one external-integration decision
Verified the shape so the decision is teed up:
SpikerSoft.AI.MCPServeris an HTTP-hosted MCP server —AddMcpServer().WithHttpTransport()+app.MapMcp("/mcp")(ModelContextProtocol.AspNetCore1.3.0), health at/api/healthz. NoUseAuthentication/UseAuthorization, no[Authorize]on/mcp.docker-compose.yml(deployment); nothing in the codebase calls/mcp.SpikerSoftAuthenticationService→AuthenticatedSpikerSoftApiClient), and act on a caller-supplieduserId— that's the impersonation lever: whoever reaches/mcpcan pass any user id.Why it can't be blind-shipped: the two halves are coupled — deriving the acting user from an authenticated principal (fixing the caller-supplied
userId) requires first authenticating/mcp, and turning on auth breaks every MCP client that doesn't present a token.ModelContextProtocol.AspNetCoresupports OAuth/bearer for HTTP transport, so the real question is a deployment one:I can implement (a) or (b) on a call, but it needs the MCP-client auth model decided first (and a smoke test that the configured clients still connect). Flagging rather than guessing.
Item 1 (API → Docker 2375) — done, merged via PR #162 (
0811a2f).Resolved the way this ticket asks (remove, not re-point) after confirming the earlier "needs frontend-contract verification" blocker was empty:
/api/DockerSwarm/*is read-only and consumed by nobody — Angular calls it 0 times (the live cluster dashboard runs onClusterHub+/api/Cluster, fed by the docker-monitor Redis snapshot), and no MCP tool calls it (server has onlyCalendarTools; the generated client's methods are dead).DockerSwarmController, its query/handler,IDockerSwarmService/DockerSwarmService(the client that connected to unauthenticatedtcp://…:2375with2375/2376/host.docker.internalfallbacks + a blocking startup probe), theIDockerClientDI wiring, and the now-deadDocker.DotNetref inSpikerSoft.Business.SpikerSoft.Data(inert; still referenced by the generated MCP client). FullTests.Unitbuild clean; 83 Cluster tests pass.The API tier no longer has any Docker-daemon dependency — matching the
.claude/rules/api-conventions.mdinvariant ("the API has NO Docker socket and must never get one").Epic status: item 1 ✅ (#162), item 2 ✅ (#158, GameServer JWT). Item 3 (MCP
/mcpauth + caller-supplieduserId) remains open — blocked on the MCP-client auth-model decision (per-user OAuth vs shared bearer vs network-restrict). Keeping this open for item 3.Board-sweep status (2026-07-22): items 1+2 merged+verified (Docker 2375 removed #162; GameServer JWKS #158). REMAINING: item 3 — SpikerSoft.AI.MCPServer/Program.cs still has NO inbound authN middleware.
Two of the three control planes are closed; please confirm the third.
grep -rn '2375\|DockerSwarmController\|DockerClientConfiguration' SpikerSoft.Apireturns nothing on master; spikersoft-backend#162 removed the legacy/api/DockerSwarmcontrol plane and the API's Docker client. Cluster topology now comes from the Redis snapshot, perapi-conventions.md.SpikerSoft.GameServer/Network/GameServerTokenValidator.csvalidates properly now (JwtSecurityTokenHandlerwithMapInboundClaims = false,TokenValidationParameterswithValidateIssuerSigningKey = trueat :60-72). No more trusting an unsignedsub.SpikerSoft.AI.MCPServerhas authentication for its outbound calls to the API (ISpikerSoftAuthenticationService, bearer token inAuthenticatedSpikerSoftApiClient), but I found no inbound authentication on the MCP server itself, andProgram.cs:79maps/api/healthzanonymously. #563 (MCPServer pre-deployment cleanup) is the natural home for it if you'd rather move it there.Worth splitting so items 1 and 2 stop carrying an open ticket.
— 2026-08-06 tracker sweep, Opus 5 Agent. Staying open.
Migrated to spikerj/spikersoft-backend#574 as part of the umbrella-tracker breakup — narrowed to item 3, since items 1 and 2 are verified done.
Verified 2026-08-07 against
spikersoft-backend@98102023:DockerSwarmController,DockerClientConfigurationor2375underSpikerSoft.Api; cluster topology reads the Redis snapshot perapi-conventions.md. Shipped by PR #162.SpikerSoft.GameServer/Network/GameServerTokenValidator.csusesJwtSecurityTokenHandlerwithMapInboundClaims = falseandTokenValidationParameterswithValidateIssuerSigningKey = true. Shipped by PR #158.SpikerSoft.AI.MCPServer/Program.csstill has noUseAuthentication/UseAuthorization;/api/healthzis mapped anonymously at :79 and/mcpis a bareapp.MapMcp("/mcp")at :81 with noRequireAuthorization().One correction worth recording: the item-3 exposure is latent, not live. The MCP server is not deployed anywhere — no swarm service, no stack file in
spikersoft-infrastructure,mcp.spikersoft.comdoes not resolve, and the only deployment reference is a commented-out block atdocker-compose.yml:52-61. So it is a pre-deployment blocker, and the "how should MCP clients authenticate" decision is the same decision as how it gets deployed.Closing here. Work now lives in the repo that holds the fix, so
fixes #574in a PR will auto-close it on merge. The umbrella tracker keeps cross-repo epics only.— Opus 5 Agent