Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@

# Banksy xmcp-to-FastMCP Migration

## How to use this roadmap

This is a **living document**. It reflects the intended final state of the migration and should be updated in-place as implementation reveals better approaches, new constraints, or scope changes.

- When deviating from the roadmap during implementation, **update the roadmap first** before proceeding. It should always describe what we're actually building, not what we originally thought we'd build.
- Mark revisions inline with a brief **`Revised:`** annotation so readers can tell what changed and why (e.g. *"Revised: switched from X to Y because Z"*). Don't silently overwrite — the revision trail is useful context.
- Implementation is driven by **phase-specific `.plan.md` files** created from this roadmap when starting each phase. The roadmap defines what to build; phase plans define how to execute each chunk.
- Research documents linked in the Deep Research Index are **not revised** — they capture point-in-time analysis. If a research finding turns out to be wrong, note it in the roadmap rather than editing the research doc.

## Summary

Rewrite banksy from a 3-process TypeScript/xmcp architecture to a Python/FastMCP server. `BANKSY_MODE` (internal/public/dev) selects the auth provider and tool set at runtime — one Docker image, multiple deployments. Two `FastMCP.from_openapi()` calls replace both `banksy-mural-api` (internal API, 39 tools) and `banksy-public-api` (Public API, 87 tools) code-gen pipelines. Auth uses FastMCP's built-in OAuth with Google as the initial IdP for Layer 1 (IDE to banksy) plus custom Python for Layer 2 (banksy to Mural API) token management. Database is a fresh PostgreSQL schema (no data migration). A React SPA is preserved for browser-facing pages (home, Session Activation, error) and served from the same process via Starlette's `StaticFiles`.
Rewrite banksy from a 3-process TypeScript/xmcp architecture to a Python/FastMCP server. `AUTH_MODE` (sso-proxy/mural-oauth/dev) selects the auth provider and tool set at runtime — one Docker image, multiple deployments. Two `FastMCP.from_openapi()` calls replace both `banksy-mural-api` (internal API, 39 tools) and `banksy-public-api` (Public API, 87 tools) code-gen pipelines. Auth uses FastMCP's built-in OAuth with Google as the initial IdP for Layer 1 (IDE to banksy) plus custom Python for Layer 2 (banksy to Mural API) token management. Database is a fresh PostgreSQL schema (no data migration). A React SPA is preserved for browser-facing pages (home, Session Activation, error) and served from the same process via Starlette's `StaticFiles`.

The repo uses a uv workspace structure under `pypackages/` — only `banksy-server` is created now. The workspace is ready to expand with `banksy-shared` (extracted shared code) and `banksy-harness` (agent orchestration) when those consumers are needed. Existing TS code in `packages/` stays as read-only reference until the final cleanup removes all TypeScript artifacts.

Expand All @@ -19,9 +27,9 @@ graph TD
Core -.->|"code-gen at build time"| PublicAPI
end

subgraph after ["Target (FastMCP, 1 image, BANKSY_MODE per deploy)"]
ClientInt["LLM Client"] -->|"MCP HTTP"| InternalDeploy["banksy BANKSY_MODE=internal"]
ClientPub["LLM Client"] -->|"MCP HTTP"| PublicDeploy["banksy BANKSY_MODE=public"]
subgraph after ["Target (FastMCP, 1 image, AUTH_MODE per deploy)"]
ClientInt["LLM Client"] -->|"MCP HTTP"| InternalDeploy["banksy AUTH_MODE=sso-proxy"]
ClientPub["LLM Client"] -->|"MCP HTTP"| PublicDeploy["banksy AUTH_MODE=mural-oauth"]
InternalDeploy -->|REST| MURAL2I["Mural API (internal)"]
PublicDeploy -->|REST| MURAL2P["Mural API (public)"]
Browser["Browser"] -->|"SPA + auth routes"| PublicDeploy
Expand Down Expand Up @@ -53,7 +61,7 @@ graph LR

| Phase | What It Delivers | Depends On | Parallelism |
|-------|-----------------|------------|-------------|
| 1 Bootstrap | uv workspace skeleton (root + `banksy-server` under `pypackages/`), echo tool, health endpoint, `BANKSY_MODE` config, CI | Nothing | -- |
| 1 Bootstrap | uv workspace skeleton (root + `banksy-server` under `pypackages/`), echo tool, health endpoint, `AUTH_MODE` config, CI | Nothing | -- |
| 2 OpenAPI Tools | `from_openapi()` integration, Mural API tools | 1 | Parallel with 4, 5 |
| 3 Tool Curation | LLM-friendly names, descriptions, transforms, composites | 2 | -- |
| 4 Database | PostgreSQL schema, Alembic migrations, token storage | 1 | Parallel with 2, 5 |
Expand All @@ -76,12 +84,12 @@ Two hard constraints shape the FastMCP migration:

### Deployment Mode Selection (Option E)

Build one Docker image. At runtime, `BANKSY_MODE` selects the auth provider and tool set. Within each mode, tags provide finer-grained client-side filtering.
Build one Docker image. At runtime, `AUTH_MODE` selects the auth provider and tool set. Within each mode, tags provide finer-grained client-side filtering.

```
BANKSY_MODE=internal -> FastMCP(auth=SSOProxyAuth) + internal tools + internal tags
BANKSY_MODE=public -> FastMCP(auth=MuralOAuthAuth) + public tools + public tags
BANKSY_MODE=dev -> FastMCP(auth=None) + all tools + all tags
AUTH_MODE=sso-proxy -> FastMCP(auth=SSOProxyAuth) + internal tools + internal tags
AUTH_MODE=mural-oauth -> FastMCP(auth=MuralOAuthAuth) + public tools + public tags
AUTH_MODE=dev -> FastMCP(auth=None) + all tools + all tags
```

### Startup Flow
Expand All @@ -96,10 +104,10 @@ def create_server() -> FastMCP:
register_common_routes(mcp) # /health, /version

match settings.banksy_mode:
case "internal":
case "sso-proxy":
register_internal_tools(mcp)
register_session_activation_routes(mcp)
case "public":
case "mural-oauth":
register_public_tools(mcp)
register_mural_oauth_routes(mcp)
case "dev":
Expand All @@ -116,9 +124,9 @@ def create_server() -> FastMCP:

### Auth Provider per Mode

**Internal mode (`sso-proxy`):** Layer 1 uses `OAuthProxy` or `RemoteAuthProvider` with Google IdP via SSO proxy. Layer 2 stores session JWTs in `mural_tokens`. Tools call `banksy-mural-api` (internal REST) with session JWTs.
**sso-proxy mode:** Layer 1 uses `OAuthProxy` or `RemoteAuthProvider` with Google IdP via SSO proxy. Layer 2 stores session JWTs in `mural_tokens`. Tools call `banksy-mural-api` (internal REST) with session JWTs.

**Public mode (`mural-oauth`):** Layer 1 uses `OAuthProxy` wrapping Mural's OAuth authorization server. Layer 2 stores Mural OAuth access/refresh tokens in `mural_tokens`. Tools call mural-api's public API with OAuth access tokens.
**mural-oauth mode:** Layer 1 uses `OAuthProxy` wrapping Mural's OAuth authorization server. Layer 2 stores Mural OAuth access/refresh tokens in `mural_tokens`. Tools call mural-api's public API with OAuth access tokens.

**Dev mode:** Layer 1 has no auth (`auth=None` or `StaticTokenVerifier`). Layer 2 tokens loaded from dev seed data. Both tool sets registered; backend URLs configurable.

Expand Down Expand Up @@ -163,8 +171,8 @@ banksy/
│ ├── src/
│ │ └── banksy_server/
│ │ ├── __init__.py
│ │ ├── server.py # Entry point: reads BANKSY_MODE, wires auth + domains
│ │ ├── config.py # pydantic-settings with BANKSY_MODE, DB URLs, auth
│ │ ├── server.py # Entry point: reads AUTH_MODE, wires auth + domains
│ │ ├── config.py # pydantic-settings with AUTH_MODE, DB URLs, auth
│ │ ├── mural_api.py # FastMCP.from_openapi() integration
│ │ ├── spa.py # SpaStaticFiles class
│ │ ├── auth/ # providers.py, sso_proxy.py, mural_oauth.py, token_manager.py
Expand Down Expand Up @@ -339,12 +347,12 @@ Two separate `from_openapi()` sub-servers, one per API spec:
- Filter to the operation IDs currently exposed by `banksy-public-api`
- Uses standard OAuth tokens for all operations

Both use `RouteMap`: GET → RESOURCE, POST/PUT/DELETE → TOOL, deprecated/internal → EXCLUDE. Each mounts onto the server within its respective `BANKSY_MODE` — `mount()` organizes tools by namespace within a single mode, not across auth modes (see Server Topology).
Both use `RouteMap`: GET → RESOURCE, POST/PUT/DELETE → TOOL, deprecated/internal → EXCLUDE. Each mounts onto the server within its respective `AUTH_MODE` — `mount()` organizes tools by namespace within a single mode, not across auth modes (see Server Topology).

**Phasing**: Start with the Public API spec in Phase 2 (when `BANKSY_MODE=public` or `dev`). Add the internal API spec as a follow-on (when `BANKSY_MODE=internal` or `dev`). The plumbing is identical — `from_openapi()` is called with different specs and different httpx clients (different base URLs, different auth injection per mode).
**Phasing**: Start with the Public API spec in Phase 2 (when `AUTH_MODE=mural-oauth` or `dev`). Add the internal API spec as a follow-on (when `AUTH_MODE=sso-proxy` or `dev`). The plumbing is identical — `from_openapi()` is called with different specs and different httpx clients (different base URLs, different auth injection per mode).

```python
# In BANKSY_MODE=public (or dev)
# In AUTH_MODE=mural-oauth (or dev)
public_api = FastMCP.from_openapi(
openapi_spec=public_spec,
client=public_http_client,
Expand All @@ -353,7 +361,7 @@ public_api = FastMCP.from_openapi(
)
mcp.mount(public_api, namespace="mural")

# In BANKSY_MODE=internal (or dev)
# In AUTH_MODE=sso-proxy (or dev)
internal_api = FastMCP.from_openapi(
openapi_spec=internal_spec,
client=internal_http_client,
Expand Down Expand Up @@ -427,9 +435,9 @@ mcp.enable(tags={"murals"}, only=True) # Mural-focused deployment

### Deployment Modes (Resolved)

Mode merging is not recommended. `BANKSY_MODE` is preserved as a runtime configuration flag. Auth modes are capability constraints — internal and public tools call different APIs with incompatible token types. FastMCP's one-auth-per-server constraint means a single server cannot cleanly handle multiple auth strategies. MCP clients support multiple servers, so separate deployments per auth mode is transparent to users.
Mode merging is not recommended. `AUTH_MODE` is preserved as a runtime configuration flag. Auth modes are capability constraints — internal and public tools call different APIs with incompatible token types. FastMCP's one-auth-per-server constraint means a single server cannot cleanly handle multiple auth strategies. MCP clients support multiple servers, so separate deployments per auth mode is transparent to users.

The current two TS Dockerfiles (`Dockerfile` for sso-proxy, `Dockerfile.mural-oauth` for mural-oauth) are replaced by a single `Dockerfile.server` — the mode is runtime config (`BANKSY_MODE` env var), not build-time. See Server Topology for the full design.
The current two TS Dockerfiles (`Dockerfile` for sso-proxy, `Dockerfile.mural-oauth` for mural-oauth) are replaced by a single `Dockerfile.server` — the mode is runtime config (`AUTH_MODE` env var), not build-time. See Server Topology for the full design.

---

Expand Down Expand Up @@ -612,7 +620,7 @@ This `get_authenticated_user` helper belongs in the auth module and is reused ac
| (new) `IDP_ISSUER` | Expected JWT issuer |
| (new) `IDP_AUDIENCE` | Expected JWT audience |
| (new) `IDP_AUTHORIZATION_SERVER` | IdP URL for PRM metadata |
| (new) `BANKSY_MODE` | `internal`, `public`, or `dev` — selects auth provider and tool set (see Server Topology) |
| (new) `AUTH_MODE` | `sso-proxy`, `mural-oauth`, or `dev` — selects auth provider and tool set (see Server Topology) |
| (new) `ENABLED_TAGS` | Optional comma-separated tag filter for specialized deployments (e.g., `read`) |

**Key TS reference**:
Expand Down Expand Up @@ -765,7 +773,7 @@ pypackages/server/src/banksy_server/domains/
└── tools.py
```

Each domain's `register_*_tools(mcp)` function takes a `FastMCP` instance and registers all tools for that domain, including tags and metadata. The domain owns its tool definitions, schemas, and any domain-specific helpers. `server.py` calls the appropriate registration functions based on `BANKSY_MODE` (see Server Topology).
Each domain's `register_*_tools(mcp)` function takes a `FastMCP` instance and registers all tools for that domain, including tags and metadata. The domain owns its tool definitions, schemas, and any domain-specific helpers. `server.py` calls the appropriate registration functions based on `AUTH_MODE` (see Server Topology).

### from_openapi() in Domain Context

Expand All @@ -789,7 +797,7 @@ def register_public_tools(mcp: FastMCP) -> None:

### Routes by Concern

Non-MCP HTTP routes (`routes/`) are organized by concern, not by mode. Mode-specific routes are registered conditionally in `server.py` based on `BANKSY_MODE` — for example, Session Activation routes are only registered in `internal` and `dev` modes.
Non-MCP HTTP routes (`routes/`) are organized by concern, not by mode. Mode-specific routes are registered conditionally in `server.py` based on `AUTH_MODE` — for example, Session Activation routes are only registered in `sso-proxy` and `dev` modes.

### Canvas-MCP Absorption

Expand Down Expand Up @@ -908,7 +916,7 @@ pypackages/server/tests/
├── test_token_refresh.py # Token refresh logic
├── test_auth_flow.py # OAuth flow (HeadlessOAuth)
├── test_session_activation.py # Session Activation routes
├── test_mode_selection.py # BANKSY_MODE startup paths
├── test_mode_selection.py # AUTH_MODE startup paths
└── test_integration/ # End-to-end tests
```

Expand All @@ -926,7 +934,7 @@ The TS codebase has ~15 Vitest test files. These should be reviewed as reference

### Dockerfile.server

Workspace-aware multi-stage build using uv. One Docker image serves all modes — `BANKSY_MODE` is a runtime env var, not build-time. This replaces the two current TS Dockerfiles (`Dockerfile` for sso-proxy, `Dockerfile.mural-oauth` for mural-oauth).
Workspace-aware multi-stage build using uv. One Docker image serves all modes — `AUTH_MODE` is a runtime env var, not build-time. This replaces the two current TS Dockerfiles (`Dockerfile` for sso-proxy, `Dockerfile.mural-oauth` for mural-oauth).

```dockerfile
# Stage 1: Build SPA
Expand Down Expand Up @@ -1124,7 +1132,7 @@ banksy-shared = { workspace = true }
- **Single workspace member → multi-member**: When a second Python service is needed (e.g., agent harness), add a directory under `pypackages/` with its own `pyproject.toml`. Extract shared code into `banksy-shared` at that time. The workspace glob auto-discovers new members.
- **pre-commit → CI only**: If hooks cause friction during rapid iteration and the team is 1–2 developers, rely on CI alone.
- **custom_route() → raw Starlette routing**: If HTTP routes grow complex, use `starlette.routing.Router` for grouping, `Mount` for sub-apps, or Starlette middleware wrappers for per-route concerns. FastAPI is a last resort — Starlette is already underneath FastMCP.
- **`BANKSY_MODE` per-deployment → mode merging**: If a future need requires multi-auth in a single process, revisit Option B (protocol-level routing) or Option D (middleware-based auth) from the [server topology analysis](../banksy-research/tool-visibility-server-topology-research.md).
- **`AUTH_MODE` per-deployment → mode merging**: If a future need requires multi-auth in a single process, revisit Option B (protocol-level routing) or Option D (middleware-based auth) from the [server topology analysis](../banksy-research/tool-visibility-server-topology-research.md).

---

Expand All @@ -1144,6 +1152,8 @@ Items from the canvas-mcp alignment assessment and architecture research that ar
| 12 | When to extract `banksy-shared` | Trigger: when a second consumer (agent harness) needs shared code (models, auth utils, Mural client) | Open (deferred) |
| 13 | When to create `banksy-harness` | Trigger: when agent orchestration work begins | Open (deferred) |

**Future naming consideration:** The research documents proposed renaming `AUTH_MODE` to something like `BANKSY_MODE` with more semantic values (`internal`/`public`/`dev`). That has merit for clarity, but we keep the existing naming (`AUTH_MODE` with `sso-proxy`/`mural-oauth`/`dev`) for migration simplicity. Consider revisiting the rename once the migration stabilizes.

---

## Deep Research Index
Expand Down
Loading