Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
e3c4e16
adding .nimblebrain to .gitignore
mgoldsborough Apr 16, 2026
0523046
Return resource_links instead of inline bytes from render tools
mgoldsborough Apr 16, 2026
3631811
Update UI to render resource_link previews and export
mgoldsborough Apr 16, 2026
4682887
Fetch preview/export bytes via synapse.readResource
mgoldsborough Apr 17, 2026
9dc1be6
Unify rendering on PDF output
mgoldsborough Apr 17, 2026
ef9dc09
Polish PDF preview frame (theme-aware, responsive)
mgoldsborough Apr 17, 2026
cb6ac93
Restructure UI into synapse-app layout
mgoldsborough Apr 17, 2026
7455a90
Replace iframe preview with unpdf-based PDFViewer
mgoldsborough Apr 17, 2026
f19cb00
Stabilize hook deps to prevent render loops
mgoldsborough Apr 17, 2026
899adc9
Collapse nested preview card — one frame, one toolbar
mgoldsborough Apr 17, 2026
15dca96
Fit button scales to viewport height instead of width
mgoldsborough Apr 17, 2026
81727ba
Flatten preview chrome and move export to the download arrow
mgoldsborough Apr 17, 2026
384af8b
Inject host theme tokens as CSS variables
mgoldsborough Apr 17, 2026
c1d9712
Restyle UI with host design-system tokens
mgoldsborough Apr 17, 2026
966955f
Responsive layout for tablet and mobile
mgoldsborough Apr 17, 2026
2c7a491
Assets as a first-class tab
mgoldsborough Apr 17, 2026
ce00b19
Expose assets via resource template + remove redundant Save
mgoldsborough Apr 17, 2026
9784ccd
Stop PDF viewer fit-scale flicker loop
mgoldsborough Apr 17, 2026
e81ff2d
Cap PDF image with CSS + floor fit scale to kill boundary flicker
mgoldsborough Apr 17, 2026
cb08893
Render PDFs directly to canvas instead of via PNG data URLs
mgoldsborough Apr 17, 2026
d54ef36
Cover collateral://assets resource + fix import order
mgoldsborough Apr 17, 2026
409a9dc
Pin @nimblebrain/synapse ^0.4.3 (released with readResource)
mgoldsborough Apr 17, 2026
185f090
Address QA review: asset path traversal + doc refresh
mgoldsborough Apr 17, 2026
b19bd3f
README: refresh tool list + add resources section
mgoldsborough Apr 17, 2026
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ Thumbs.db

# MCP/Claude
.claude/
.nimblebrain/

# Package managers
uv.lock
Expand Down
11 changes: 11 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@

Fat MCP server for document generation. Documents are the primary entity. The agent writes Typst; the server compiles it.

## Breaking changes

- **v0.4+ `export_pdf` no longer accepts `include_data: bool`.** Bytes always
move out-of-band via the `collateral://exports/{id}.pdf` resource template —
inlining base64 PDFs in tool results blew through the host's 1 MB cap.
Direct MCP callers passing `include_data` will now fail input validation;
LLM-facing behavior is unchanged.
- **v0.4+ rendering unified on PDF output.** `preview`, `preview_template`,
`export_pdf`, and `compile_typst` all return a single `resource_link` block
pointing at a PDF. The previous per-page PNG output path is gone.

## Commands

```bash
Expand Down
25 changes: 20 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,26 @@ Typst-powered document generation with brand-aware templates, live preview, and

## Tools

- **Workspace**: `get_workspace`, `reset_workspace`
- **Templates**: `list_templates`, `get_template_schema`, `set_template`
- **Brand**: `get_brand`, `set_brand`, `update_brand_colors`, `set_logo`, `list_brand_presets`, `load_brand_preset`
- **Content**: `set_content`, `update_section`, `get_content`, `list_sections`
- **Rendering**: `preview`, `preview_page`, `export_pdf`, `compile_typst`
- **Theme**: `get_theme`, `set_theme`
- **Templates**: `list_templates`, `get_template`, `create_template`, `duplicate_template`, `delete_template`
- **Documents**: `create_document`, `list_documents`, `open_document`, `save_document`, `save_as_template`, `delete_document`
- **Workspace & Editing**: `get_workspace`, `get_source`, `patch_source`, `set_source`, `import_content`
- **Assets**: `upload_asset`, `list_assets`, `delete_asset`
- **Voice & Components**: `get_voice`, `set_voice`, `get_components`, `set_components`
- **Fonts**: `list_fonts`, `install_font`
- **Rendering**: `preview`, `preview_template`, `export_pdf`, `compile_typst`

Rendering tools return MCP `resource_link` content blocks pointing at the export resource template — not inline bytes. Clients fetch PDFs via `resources/read`.

## Resources

- `ui://collateral/main` — Studio UI rendered in the platform sidebar
- `ui://collateral/settings` — brand / voice / assets configuration panel
- `ui://collateral/preview.pdf` — current document compiled to PDF
- `collateral://exports/{export_id}.{ext}` — rendered export bytes, MIME per extension
- `collateral://assets/{filename}` — uploaded asset bytes
- `skill://collateral/usage` — agent-facing usage guide
- `skill://collateral/reference` — tool catalog, error recovery, anti-patterns

## Built-in Templates

Expand Down
55 changes: 14 additions & 41 deletions src/mcp_collateral/compiler.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Typst compilation pipeline.

Compiles Typst source to PDF or PNG. Knows nothing about templates,
starters, or workspaces -- just takes source and produces bytes.
Compiles Typst source to PDF. Knows nothing about templates, starters,
or workspaces -- just takes source and produces bytes.

The compiler uses --root pointing to BASE_DIR so that absolute Typst
paths like /assets/ resolve correctly. If components.typ exists in
Expand Down Expand Up @@ -43,53 +43,38 @@ def _clean_compile_dir() -> None:
def compile_source(
source: str,
logo_data: dict[str, bytes] | None = None,
output_format: str = "pdf",
page: int | None = None,
) -> list[bytes]:
"""Compile Typst source to PDF or PNG pages.
) -> bytes:
"""Compile Typst source to a PDF.

Returns a list of bytes objects (one for PDF, one per page for PNG).

Writes source to COMPILE_DIR (~/.collateral/_compile/), copies
components.typ if it exists, compiles with --root pointing to
BASE_DIR, and cleans up after.
When ``page`` is provided, the output is a single-page PDF containing
only that page (1-based). Otherwise the full document is rendered.
"""
typst_bin = _find_typst()

try:
_clean_compile_dir()

# Copy components.typ into compile dir if it exists
components_path = BASE_DIR / "components.typ"
if components_path.exists():
shutil.copy2(components_path, COMPILE_DIR / "components.typ")

# Write brand assets to _compile/brand/
brand_dir = COMPILE_DIR / "brand"
brand_dir.mkdir(parents=True, exist_ok=True)
if logo_data:
for filename, data in logo_data.items():
(brand_dir / filename).write_bytes(data)

# Write source
(COMPILE_DIR / "document.typ").write_text(source)

if output_format == "pdf":
out_path = COMPILE_DIR / "output.pdf"
_run_typst(typst_bin, "_compile/document.typ", str(out_path))
return [out_path.read_bytes()]
else:
out_pattern = str(COMPILE_DIR / "output-{n}.png")
pages_arg = str(page) if page is not None else None
_run_typst(
typst_bin,
"_compile/document.typ",
out_pattern,
fmt="png",
pages=pages_arg,
)
png_files = sorted(COMPILE_DIR.glob("output-*.png"))
return [p.read_bytes() for p in png_files]
out_path = COMPILE_DIR / "output.pdf"
_run_typst(
typst_bin,
"_compile/document.typ",
str(out_path),
pages=str(page) if page is not None else None,
)
return out_path.read_bytes()
finally:
_clean_compile_dir()

Expand All @@ -98,18 +83,8 @@ def _run_typst(
typst_bin: str,
input_file: str,
output: str,
fmt: str = "pdf",
pages: str | None = None,
) -> None:
"""Run the typst compiler.

Args:
typst_bin: Path to the typst binary.
input_file: Input file path relative to BASE_DIR (e.g. "_compile/document.typ").
output: Absolute path for the output file.
fmt: Output format ("pdf" or "png").
pages: Optional page range for PNG output.
"""
cmd = [
typst_bin,
"compile",
Expand All @@ -118,8 +93,6 @@ def _run_typst(
"--font-path",
str(FONTS_DIR),
]
if fmt == "png":
cmd.extend(["--format", "png"])
if pages:
cmd.extend(["--pages", pages])
cmd.extend([str(BASE_DIR / input_file), output])
Expand Down
22 changes: 0 additions & 22 deletions src/mcp_collateral/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,25 +73,3 @@ class WorkspaceState(BaseModel):
document_name: str | None = None
template_id: str | None = None
theme: ThemeData = Field(default_factory=ThemeData)


# --- Rendering ---


class PagePreview(BaseModel):
page_number: int
image_base64: str | None = None


class PreviewResult(BaseModel):
pages: list[PagePreview] = Field(default_factory=list)
page_count: int = 0
message: str = ""


class ExportResult(BaseModel):
pdf_base64: str | None = None
filename: str = "document.pdf"
page_count: int = 0
size_bytes: int = 0
message: str = ""
Loading