README.md•9.29 kB
---
version: v0.2.7
owner: delk73
lastReviewed: 2025-09-12
---
# Synesthetic MCP
Minimal, deterministic MCP-style adapter exposing schemas, examples, validation, diff, and an optional backend-populate tool.
## System Context
```mermaid
flowchart LR
MCP["synesthetic-mcp\n(MCP Adapter: discovery, validation, diff, backend proxy)"]
BE["sdfk-backend\n(Python API & CRUD store)"]
SSOT["synesthetic-schemas\n(SSOT: JSON Schemas + Python bindings)"]
MCP --> |"Validates assets (Protobuf/JSON)"| BE
MCP --> |"Consumes Python pkg (submodule)"| SSOT
BE --> |"Consumes Python pkg (submodule)"| SSOT
%% Grayscale theme, MCP slightly lighter to highlight
style MCP fill:#555555,stroke:#ffffff,stroke-width:2px,color:#ffffff
style BE fill:#333333,stroke:#cccccc,stroke-width:1.5px,color:#ffffff
style SSOT fill:#222222,stroke:#cccccc,stroke-width:1.5px,color:#ffffff
```
## Features
- Schema and example discovery
- JSON Schema validation (Draft 2020-12)
- Batch validation via `validate_many` with `MCP_MAX_BATCH` (default 100)
- RFC6902 diff (add/remove/replace only)
- Backend population (optional via `SYN_BACKEND_URL`)
- Canonical STDIO JSON-RPC loop with optional Unix-domain socket transport (`MCP_ENDPOINT=socket`) and TCP transport (`MCP_ENDPOINT=tcp`, `MCP_HOST`, `MCP_PORT`)
- Per-request 1 MiB payload guard enforced before parsing across STDIO, socket, and TCP transports
- Deprecated `validate` alias remains available but logs a warning; prefer `validate_asset`
- Strict asset contract: top-level `$schema` is required and legacy `schema`/`$schemaRef` keys are rejected (v0.2.8)
## Quickstart
1. Install deps: `pip install -r requirements.txt && pip install -e .`
2. Initialize schemas/examples: `git submodule update --init --recursive`.
3. Serve via Compose: `docker compose up serve` (runs the transport, logs `mcp:ready mode=<endpoint>` with ISO-8601 timestamps, and exposes `/tmp/mcp.ready` for health checks).
4. Or run the helper: `./up.sh` builds the image and starts the serve service in the background; follow with `docker compose logs -f serve` if you want to tail logs.
5. Validate an asset locally: `python -m mcp --validate libs/synesthetic-schemas/examples/SynestheticAsset_Example1.json`.
## Structure
```
README.md
requirements.txt
docs/
mcp_spec.md
mcp/
__init__.py
core.py
validate.py
diff.py
backend.py
stdio_main.py
socket_main.py
transport.py
tests/
test_validate.py
test_diff.py
test_backend.py
test_env_discovery.py
test_stdio.py
test_submodule_integration.py
```
## Development
* Python >= 3.11
* Install deps (minimal): `pip install -r requirements.txt`
- Minimal deps: `jsonschema`, `httpx`, `pytest`
- Optional extras: `referencing` (enhanced JSON Schema refs; import is optional)
- Dev (optional): `ruff`, `mypy`
* Import check: `python -c "import mcp; print(mcp.__version__)"`
* Run tests: `pytest -q`
* Runtimes:
- `python -m mcp` (STDIO by default; set `MCP_ENDPOINT=socket` for the Unix-domain socket server or `MCP_ENDPOINT=tcp` for TCP. Logs `mcp:ready mode=<endpoint>` with ISO-8601 timestamps on readiness).
- `python -m mcp.stdio_main` (invoke the STDIO loop directly when embedding).
## Dependencies
- Runtime: `jsonschema`, `httpx`
- Tests: `pytest`
- Dev (optional): `ruff`, `mypy`
- Extras (optional): `referencing` (ref handling performance/behavior)
## Environment
| Variable | Default | Behaviour |
| - | - | - |
| `MCP_ENDPOINT` | `stdio` | Transport selector. `stdio` runs over stdin/stdout; `socket` enables the Unix-domain socket server; `tcp` binds a TCP listener. |
| `MCP_READY_FILE` | `/tmp/mcp.ready` | File touched on startup with `<pid> <ISO8601>` and removed on shutdown; Compose health checks test for its presence. Override when sandboxed. |
| `MCP_SOCKET_PATH` | `/tmp/mcp.sock` | Socket path when `MCP_ENDPOINT=socket`. The server unlinks the file on shutdown. |
| `MCP_SOCKET_MODE` | `0600` | Octal file mode applied to the socket on startup. Increase only when the socket must be shared. |
| `MCP_HOST` | `0.0.0.0` | TCP bind address when `MCP_ENDPOINT=tcp`. Use `127.0.0.1` for local development. |
| `MCP_PORT` | `7000` | TCP port when `MCP_ENDPOINT=tcp`. Set to `0` to request an ephemeral port (logged on startup). |
| `SYN_SCHEMAS_DIR` | `libs/synesthetic-schemas/jsonschema` when present | Overrides schema directory; required when submodule absent. Startup fails if the directory is missing. |
| `SYN_EXAMPLES_DIR` | `libs/synesthetic-schemas/examples` when present | Overrides examples directory; discovery falls back to submodule if unset. |
| `SYN_BACKEND_URL` | unset | Enables backend POSTs; missing keeps populate disabled (`unsupported`). |
| `SYN_BACKEND_ASSETS_PATH` | `/synesthetic-assets/` | Custom path for backend POST requests. |
| `MCP_MAX_BATCH` | `100` | Maximum batch size for `validate_many`; oversized batches return `{ok:false, reason:'unsupported'}`. |
`.env.example` captures these defaults for quick copying into local shells or Compose.
### Environment Discovery
- `SYN_SCHEMAS_DIR` and `SYN_EXAMPLES_DIR` override paths when set.
- Otherwise, schemas/examples are loaded from the `libs/synesthetic-schemas` submodule.
- If neither is available, listings are empty and get operations return not found (no fixture fallback).
### Submodule (SSOT)
Authoritative schemas/examples live at `libs/synesthetic-schemas` (git submodule).
Order of discovery used by the adapter:
1) `SYN_SCHEMAS_DIR` and `SYN_EXAMPLES_DIR` if set
2) `libs/synesthetic-schemas/jsonschema` and `libs/synesthetic-schemas/examples` if present
3) If neither exists, listings are empty and get operations return not found
Initialize the submodule:
```
git submodule update --init --recursive
```
Update the submodule to the latest published commit (`0fdc842` as of this audit):
```
git -C libs/synesthetic-schemas fetch origin
git -C libs/synesthetic-schemas checkout 0fdc842
git add libs/synesthetic-schemas
```
After checking out the new commit you can create a commit in the main repository, for example `git commit -m "chore: bump synesthetic-schemas to 0fdc842"`.
### Schema Aliases (Nested Assets)
* **`synesthetic-asset`** → canonical schema (flat).
* **`nested-synesthetic-asset`** → alias for assets with components inlined.
* All component types may be embedded (shader, tone, haptic, control, modulation, rule bundle).
* Alias validation loads the canonical `synesthetic-asset` schema.
* Examples `SynestheticAsset_Example*.json` are treated as `nested-synesthetic-asset`.
* Assets MUST declare their validating schema via top-level `$schema`; legacy `schema` or `$schemaRef` keys are rejected.
* Tests use the nested alias; submodule is the single source of truth.
## Error Model
- Validation failed: `{ ok:false, reason:'validation_failed', errors:[{ path, msg }] }`
- Backend error: `{ ok:false, reason:'backend_error', status, detail }`
- Unsupported tool/resource: `{ ok:false, reason:'unsupported', detail }`
- Network errors map to backend_error with `status:503` and a brief `detail`.
- Payload too large (>1 MiB on STDIO/socket/TCP): `{ ok:false, reason:'validation_failed', errors:[{ path:'', msg:'payload_too_large' }] }`
## CLI Usage
```
$ python -m mcp --validate path/to/asset.json
{"ok": true, "errors": []}
```
- Exit code `0`: validation succeeded.
- Exit code `1`: validation failed (payload includes `reason: validation_failed`).
- Exit code `2`: input errors (file missing, unreadable, or invalid JSON).
### Docker
Build and run tests in a container:
```
./test.sh
```
Notes:
- `docker-compose.yml` passes through env if set; there are no defaults to fixtures. The adapter’s own discovery logic picks the right source.
- No backend service is started by compose; backend calls are disabled unless `SYN_BACKEND_URL` is set.
### Serving Locally
- `docker compose up serve` builds the image, starts `python -m mcp`, waits for `/tmp/mcp.ready`, and keeps logs attached.
- `./up.sh` builds the image and starts the `serve` service in detached mode; run `docker compose logs -f serve` to follow output after startup.
- STDIO remains the default; set `MCP_ENDPOINT=socket` to listen on the Unix-domain socket path, or `MCP_ENDPOINT=tcp` (optionally with `MCP_HOST`/`MCP_PORT`) to expose a TCP listener.
- Requests above 1 MiB (UTF-8 bytes) are rejected before parsing with `payload_too_large` on STDIO, socket, and TCP (see `tests/test_stdio.py`, `tests/test_socket.py`, and `tests/test_tcp.py`).
## Spec
See `docs/mcp_spec.md` for deterministic IO contracts and limits.
## Status
✅ Spec pinned in `docs/mcp_spec.md`
✅ Minimal implementation with tests
### Validating it
**Quick host check with `nc`:**
```bash
nc localhost 8765
```
Paste this request:
```json
{"jsonrpc":"2.0","id":1,"method":"list_schemas"}
```
You should get a JSON response with available schemas.
**Labs connection env** (pointing to the MCP container via TCP):
```bash
MCP_ENDPOINT=tcp
MCP_HOST=localhost # or 'synesthetic-mcp-serve-1' if inside Docker network
MCP_PORT=8765
LABS_FAIL_FAST=1
SYN_SCHEMAS_DIR=libs/synesthetic-schemas/jsonschema
SYN_EXAMPLES_DIR=libs/synesthetic-schemas/examples
```
**Run Labs against it:**
```bash
python -m labs.cli generate --prompt "hello world shader test"
```