Skip to main content
Glama
README.md
# MVR — Marketing Workflow & Visual Reference Vault

MVR is a self-hosted, local-first workspace and MCP server for keeping AI-assisted marketing work consistent with reusable brand context, visual references, evaluation rules, and human approval.

> MVR is not a video or image generation provider. It stores and governs the context around creative work. Generation can be delegated to an external provider through a future or custom provider adapter.

## What MVR provides

- Reusable creative **Elements** referenced with `@tags`.
- Versioned PNG reference assets organized into named slots.
- Prompt resolution with missing-Element and incomplete-slot warnings.
- A typed marketing workflow powered by **LangGraphJS**.
- Deterministic evaluation with explainable issues and recommendations.
- Human-review routing with persisted decisions and feedback.
- Workflow checkpoints and event history in SQLite.
- Draft-provider abstraction with a safe local deterministic provider.
- MCP tools over stdio and authenticated Streamable HTTP.
- REST endpoints and a local dashboard for operating the workflow.
- Local filesystem or S3-compatible asset storage.
- Encrypted provider credentials with masked API-key responses.

## Product flow

```text
Creative references + marketing brief
                    ↓
          Resolve @Element context
                    ↓
              Create a draft
                    ↓
          Evaluate issues and risks
                    ↓
       Complete or request human review
```

## Current workflow

The runnable vertical slice is:

```text
Marketing brief
  → normalizeBrief
  → loadBrandContext
  → createDraft
  → evaluateDraft
  → decideNextStep
  → completed / waiting_for_human / failed
```

![MVR marketing workflow graph](docs/images/marketing-workflow-graph.svg)

The graph is implemented in [`src/workflows/marketing-graph.ts`](src/workflows/marketing-graph.ts) with LangGraphJS `StateGraph`. Each business node follows this application-owned contract:

```text
(state, context) → partial state update
```

### Node responsibilities

| Node | Responsibility |
|---|---|
| `normalizeBrief` | Trim and validate objective, audience, channel, and request. |
| `loadBrandContext` | Resolve `@Element` tags and collect assets, missing tags, and completeness warnings. |
| `createDraft` | Call the injected `DraftProvider`; the default provider creates a deterministic local draft. |
| `evaluateDraft` | Check unknown Elements and missing required slots. |
| `decideNextStep` | Complete clean runs or create a pending `ReviewTask`. |

Every node checkpoint is persisted. The latest workflow state and the ordered event history can be inspected after a restart.

## Architecture

MVR is a modular monolith. HTTP and MCP are adapters; application behavior is kept behind domain and application modules.

```mermaid
flowchart TD
    User[Marketing team]
    Web[Local dashboard]
    Client[MCP-compatible AI client]
    REST[REST adapter]
    MCP[MCP adapter]
    Service[MarketingWorkflowService]
    Graph[LangGraphJS StateGraph]
    Vault[(SQLite Vault)]
    Assets[(Local or S3 assets)]
    Provider[Injected DraftProvider]

    User --> Web
    User --> Client
    Web --> REST
    Client --> MCP
    REST --> Service
    MCP --> Service
    Service --> Graph
    Graph --> Vault
    Graph --> Provider
    Vault --> Assets
```

### Main modules

| Module | Role |
|---|---|
| `src/index.ts` | HTTP server, dashboard route, REST routes, and stdio entry point. |
| `src/mcp.ts` | MCP tool definitions and MCP adapter. |
| `src/dashboard.ts` | Self-contained local HTML dashboard. |
| `src/vault.ts` | SQLite facade, migrations, persistence, asset coordination, and event history. |
| `src/storage.ts` | Local and S3-compatible `AssetStorage` implementations. |
| `src/crypto.ts` | AES-GCM provider-secret encryption and masking. |
| `src/workflows/marketing-graph.ts` | Typed workflow state, nodes, graph assembly, and runner. |
| `src/workflows/marketing-service.ts` | Shared application boundary for REST, MCP, and web clients. |
| `src/workflows/draft-provider.ts` | Draft provider contract and deterministic local implementation. |

### Boundary rules

1. REST and MCP handlers call `MarketingWorkflowService`, not graph internals.
2. Business logic is not duplicated between adapters.
3. `Vault` owns SQLite-backed persistence coordination.
4. `AssetStorage` hides local filesystem and S3 implementation details.
5. Provider secrets are encrypted at rest and never returned in plaintext.
6. Evaluation returns explainable issues, not only a score.
7. Missing creative context is surfaced as a warning and can route to review.
8. External generation providers must be accessed through an adapter.
9. Do not add queues, microservices, PostgreSQL, or vector infrastructure without measured operational requirements.

## Core concepts

### Elements

An Element is a reusable creative reference:

- `character`
- `prop`
- `location`
- `style`
- `audio`
- `other`

Example:

```text
Create a product video featuring @hero and @headphones.
```

An Element can have versioned assets in slots:

```text
@hero
├── face_closeup
├── body_front
└── body_back
```

Kind templates define required and optional slots. A character, for example, requires `face_closeup`, `body_front`, and `body_back`. Missing required slots produce completeness warnings.

### Prompt resolution

`resolve_prompt` and `Vault.resolvePrompt()` replace known `@tags` with reusable context and expose:

- `resolvedText`
- `elementsUsed`
- attached reference assets
- `missing` unknown tags
- `completenessWarnings`

Unknown or incomplete context is not silently discarded.

### Workflow state

A workflow run is persisted under a caller-provided or generated `runId`.

The state contains:

- marketing brief
- resolved prompt context
- draft
- draft provenance (`provider`, `model`, `generatedAt`)
- evaluation result
- human-review requirement
- structured `ReviewTask`
- current node
- status and errors

Statuses are:

```text
running
completed
waiting_for_human
failed
```

The `workflow_events` table stores ordered node snapshots such as:

```text
start
normalizeBrief
loadBrandContext
createDraft
evaluateDraft
decideNextStep
```

### Draft providers

The graph depends on the `DraftProvider` interface rather than a specific model vendor:

```ts
interface DraftProvider {
  createDraft(request: DraftRequest): Promise<DraftResult>;
}
```

The default `DeterministicDraftProvider` is intentionally local and predictable. It does not call an external model. A real provider can be injected through `MarketingWorkflowService` without changing the graph or adapters.

### Evaluation

The current evaluator is deterministic and checks:

- unknown `@Elements`
- incomplete required Element slots

A clean context receives score `1` and completes. Incomplete context receives score `0.5` and waits for human review. Future evaluators can add brand voice, channel fit, factuality, compliance, and model-based judging while retaining the deterministic gate.

## Dashboard

The local dashboard is served from `/` and supports:

- create and delete Elements
- inspect required and optional slots
- upload versioned PNG assets
- lock an asset slot after approval
- save encrypted provider credentials
- configure task-specific models
- submit a marketing brief
- inspect workflow status, draft, evaluation, errors, and provenance
- inspect checkpoint history
- approve or reject pending human review with feedback

Open it after starting the server:

```text
http://localhost:8080
```

## MCP tools

The server supports MCP over stdio and authenticated Streamable HTTP.

### Creative library

| Tool | Purpose |
|---|---|
| `list_elements` | List reusable Elements. |
| `get_element` | Get an Element and version history. |
| `create_element` | Create a character, prop, location, style, audio, or other Element. |
| `update_element` | Update Element metadata or summary. |
| `delete_element` | Delete an Element and its records. |
| `upload_element_asset` | Upload a PNG to a named Element slot. |
| `list_element_assets` | List the current asset in each filled slot. |
| `check_element_completeness` | Check required slots against the kind template. |
| `get_kind_template` | Get slot recipes and best practices. |
| `get_prompt_format` | Get a provider/model prompt format template. |
| `resolve_prompt` | Resolve `@tags` into context and asset references. |

### Workflow

| Tool | Purpose |
|---|---|
| `run_marketing_workflow` | Run and persist the marketing graph. |
| `get_workflow_run` | Read one persisted workflow run. |
| `list_workflow_runs` | List runs ordered by recent update. |
| `get_workflow_events` | Read ordered node checkpoint history. |
| `resume_marketing_workflow` | Approve or reject a pending human review. |

### Provider and model configuration

| Tool | Purpose |
|---|---|
| `list_model_configs` | List task-specific model configurations. |
| `set_model_config` | Save a provider/model configuration. |
| `get_active_model` | Get the active model for a task. |
| `delete_model_config` | Delete a model configuration. |
| `list_providers` | List providers with masked API keys. |

Example workflow input:

```json
{
  "objective": "Introduce the campaign hero",
  "audience": "Existing customers",
  "channel": "social",
  "request": "Create a short post featuring @hero."
}
```

## REST API

All REST routes are under `/api`. If `MVR_AUTH_TOKEN` is configured, they require a Bearer token.

| Method | Endpoint | Purpose |
|---|---|---|
| `GET`, `POST` | `/elements` | List or create Elements. |
| `DELETE` | `/elements/:tagOrId` | Delete an Element. |
| `GET`, `POST` | `/elements/:tagOrId/assets` | List or upload Element assets. |
| `GET` | `/elements/:tagOrId/completeness` | Check required slots. |
| `GET` | `/kind-templates/:kind` | Get an Element kind template. |
| `GET` | `/prompt-formats?provider=Seedance&model=2.0` | Get a prompt format. |
| `GET`, `POST` | `/providers` | List or save encrypted provider credentials. |
| `DELETE` | `/providers/:name` | Delete a provider. |
| `GET`, `POST` | `/model-configs` | List or save model configurations. |
| `DELETE` | `/model-configs/:task` | Delete a model configuration. |
| `POST` | `/workflows/marketing` | Start a workflow run. |
| `GET` | `/workflows/marketing` | List workflow runs. |
| `GET` | `/workflows/marketing/:runId` | Read a workflow run. |
| `GET` | `/workflows/marketing/:runId/events` | Read checkpoint/event history. |
| `POST` | `/workflows/marketing/:runId/review` | Approve or reject human review. |

Example:

```bash
curl -X POST http://localhost:8080/api/workflows/marketing \
  -H 'Content-Type: application/json' \
  -d '{
    "objective": "Launch the hero",
    "audience": "Existing customers",
    "channel": "social",
    "request": "Create a post featuring @hero."
  }'
```

## Quickstart

### Local

```bash
cp .env.example .env
# Set MVR_ENCRYPTION_KEY and MVR_AUTH_TOKEN in .env.
npm ci
npm run build
npm start
```

The server listens on `0.0.0.0:8080` by default.

Development mode:

```bash
npm run dev
```

Run tests and build:

```bash
npm test
npm run build
```

Run the stdio MCP server:

```bash
npm run stdio
```

### Docker

```bash
cp .env.example .env
# Set MVR_ENCRYPTION_KEY and MVR_AUTH_TOKEN.
docker compose up --build
```

Docker Compose persists SQLite data in the `mvr-data` volume and binds local assets through `./data/assets`.

### MCP client configuration

For a stdio-compatible client:

```json
{
  "mcpServers": {
    "mvr": {
      "command": "node",
      "args": ["/absolute/path/to/MVR/dist/index.js", "--stdio"],
      "env": {
        "MVR_DB_PATH": "/absolute/path/to/MVR/data/mvr.db",
        "MVR_ASSET_PATH": "/absolute/path/to/MVR/data/assets",
        "MVR_ENCRYPTION_KEY": "your-long-random-secret"
      }
    }
  }
}
```

For Streamable HTTP:

```text
http://localhost:8080/mcp
Authorization: Bearer your-long-random-token
Accept: application/json, text/event-stream
```

## Configuration

| Variable | Purpose | Default |
|---|---|---|
| `PORT` | HTTP port. | `8080` |
| `MVR_AUTH_TOKEN` | Bearer token for HTTP and MCP. | unset |
| `MVR_ENCRYPTION_KEY` | Key for encrypted provider credentials. | development fallback; set in production |
| `MVR_DB_PATH` | SQLite database path. | `data/mvr.db` |
| `MVR_STORAGE_DRIVER` | `local` or `s3`. | `local` |
| `MVR_ASSET_PATH` | Local asset root. | `data/assets` |
| `MVR_ASSET_IMPORT_PATH` | Trusted local PNG import directory. | `data/imports` |
| `MVR_S3_BUCKET` | S3-compatible bucket. | unset |
| `MVR_S3_REGION` | S3-compatible region. | `us-east-1` |
| `MVR_S3_ACCESS_KEY_ID` | S3-compatible access key. | unset |
| `MVR_S3_SECRET_ACCESS_KEY` | S3-compatible secret. | unset |
| `MVR_S3_ENDPOINT` | Optional S3-compatible endpoint. | unset |
| `MVR_MIN_EVAL_RUNS_BEFORE_AUTO_UPDATE` | Reserved threshold for future automatic recipe updates. | `10` |

## Security and operational notes

- Set `MVR_AUTH_TOKEN` before exposing the server beyond localhost.
- Set a high-entropy `MVR_ENCRYPTION_KEY` in production and back it up securely.
- Changing the encryption key makes existing encrypted credentials unreadable.
- Provider keys are encrypted at rest and masked in responses.
- Only valid PNG assets up to 10 MB are accepted.
- File references are constrained to `MVR_ASSET_IMPORT_PATH`.
- Asset versions are preserved; locked slots cannot be overwritten.
- Use the human-review route before publishing or taking irreversible actions.
- The current local dashboard is intended for a trusted local workspace; multi-user authorization is not implemented by default.

## Tests

The test suite covers:

- Element versioning and prompt resolution.
- Provider encryption and masking.
- Model configuration.
- Asset versioning, PNG validation, and locking.
- Kind templates and completeness warnings.
- Successful, failed, and human-review workflow paths.
- LangGraph execution and node checkpoints.
- Draft-provider injection and provenance.
- Workflow event history.
- Idempotent run IDs and brief-conflict protection.
- SQLite persistence across vault reopen.

Run:

```bash
npm test
npm run build
```

## Roadmap

### Next

- Replace the deterministic provider with external LLM/video provider adapters.
- Add prompt/model version hashes and richer generation provenance.
- Use LangGraph interrupt/checkpointer resume semantics for a true durable human-in-the-loop pause.
- Add optimistic locking and stronger concurrency controls.
- Add brand, campaign, and brief entities.
- Add brand voice, compliance, factuality, and channel-fit evaluators.

### Later

- Bounded revise loops after human feedback.
- Multiple AI provider adapters.
- Integrations for Slack, Notion, Drive, Figma, HubSpot, and analytics.
- Golden examples and regression evaluation datasets.
- Brand drift detection and campaign-performance feedback.
- Multi-user roles and approval policies.

## Scope boundary

MVR currently does not:

- generate videos or images by itself
- replace external AI generation providers
- automatically publish marketing content
- automatically modify brand rules from model output
- provide multi-user authorization by default

## License

See the repository license for usage terms.