Agent-facing surface#

The MCP server and the spec pack it serves. See the MCP server reference for the tools as a client sees them, and driving Scenet from a model for connecting one.

scenet.mcp needs the optional extra, pip install 'scenet[mcp]'; scenet.spec_pack does not.

scenet.mcp#

.. py:module:: scenet.mcp

An MCP server, so a model can check and render the panels it writes.

The spec pack tells a model what the language is; this lets it find out what it got wrong. A model writes a document, calls validate, reads findings that each name a rule, a line and a fix, corrects its own output and calls render – with no human copying error text between a terminal and a chat window. It is the only part of the agent-facing surface that closes that loop.

Install it with the extra, and start it with the scenet mcp command:

   pip install 'scenet[mcp]'
   scenet mcp                             # stdio, for a local client
   scenet mcp --transport streamable-http # for a remote one

The official SDK, and only as an extra#

Built on the official mcp package, which already speaks protocol revision 2026-07-28 – the stateless core, the extensions framework, cacheable list results – and adds nothing FastMCP would have to supply. It is an extra rather than a dependency because it brings an HTTP stack the compiler has no use for: the browser playground installs the base wheel into Pyodide, and someone who only wants scenet build should not have to put Starlette through their own licence review.

Transports are stdio and Streamable HTTP. Not SSE: the protocol has deprecated it, and Streamable HTTP replaced it.

Five tools, shaped after compilers that are already served this way#

typst-mcp serves its documentation one chapter at a time, checks a snippet, and renders one; d2-mcp has a cheat sheet, compile to validate and render to draw. The same shapes, rather than new ones:

  • get_spec – the spec pack, one part at a time, so a model need not read all of it

  • list_puppets – the characters it may cast, with their poses and expressions

  • validate – every finding at once, shaped rather than dumped as raw SARIF

  • compile – the Panel Core: where the compiler put everything, and why

  • render – the SVG, byte-identical to what scenet build writes

Each takes the document as text, not a path. A remote client shares no file system with the server, and one calling convention for both transports is one less thing to get wrong. Every tool is read-only and closed-world, and says so in its annotations: nothing here writes a file or reaches the network.

Keeping stdout clean#

On stdio, stdout is the protocol stream, and one stray line corrupts it. That is why nothing here goes through cli.run_build, which prints wrote and note: lines – the easiest way there is to ship a broken stdio server. Notes travel in the tool result instead. The SDK also points file descriptor 1 at stderr while it serves, which catches a stray print from a dependency; the test suite drives a real stdio session against the installed command to make sure neither ever matters.

.. py:class:: CompileReport

module:

scenet.mcp

Bases: :py:class:~pydantic.main.BaseModel

Every panel in a document, compiled.

.. py:attribute:: CompileReport.panels

module:

scenet.mcp

type:

list[~scenet.mcp.CompiledPanel]

.. py:class:: CompiledPanel

module:

scenet.mcp

Bases: :py:class:~pydantic.main.BaseModel

One compiled panel.

.. py:attribute:: CompiledPanel.name

module:

scenet.mcp

type:

str

.. py:attribute:: CompiledPanel.notes

module:

scenet.mcp

type:

list[str]

.. py:attribute:: CompiledPanel.core

module:

scenet.mcp

type:

dict[str, ~typing.Any]

.. py:class:: Finding

module:

scenet.mcp

Bases: :py:class:~pydantic.main.BaseModel

One thing wrong with a document: which rule, where, and what to do about it.

.. py:attribute:: Finding.rule

module:

scenet.mcp

type:

str

.. py:attribute:: Finding.message

module:

scenet.mcp

type:

str

.. py:attribute:: Finding.fix

module:

scenet.mcp

type:

str

.. py:attribute:: Finding.where

module:

scenet.mcp

type:

str

.. py:attribute:: Finding.line

module:

scenet.mcp

type:

int

.. py:attribute:: Finding.column

module:

scenet.mcp

type:

int

.. py:attribute:: Finding.end_line

module:

scenet.mcp

type:

int

.. py:attribute:: Finding.end_column

module:

scenet.mcp

type:

int

.. py:class:: Puppet

module:

scenet.mcp

Bases: :py:class:~pydantic.main.BaseModel

A character a cast member’s reference can name.

.. py:attribute:: Puppet.name

module:

scenet.mcp

type:

str

.. py:attribute:: Puppet.heads_tall

module:

scenet.mcp

type:

float

.. py:attribute:: Puppet.poses

module:

scenet.mcp

type:

list[str]

.. py:attribute:: Puppet.expressions

module:

scenet.mcp

type:

list[str]

.. py:class:: PuppetCatalogue

module:

scenet.mcp

Bases: :py:class:~pydantic.main.BaseModel

The puppet library.

.. py:attribute:: PuppetCatalogue.puppets

module:

scenet.mcp

type:

list[~scenet.mcp.Puppet]

.. py:class:: ValidationReport

module:

scenet.mcp

Bases: :py:class:~pydantic.main.BaseModel

Everything wrong with a document, all at once.

.. py:attribute:: ValidationReport.valid

module:

scenet.mcp

type:

bool

.. py:attribute:: ValidationReport.findings

module:

scenet.mcp

type:

list[~scenet.mcp.Finding]

.. py:function:: build_server()

module:

scenet.mcp

Build the server with every tool registered, without starting it.

Separate from :func:serve <scenet.mcp.serve> so that a test, or a host embedding Scenet in a server of its own, can connect to it in process.

rtype:
sphinx_autodoc_typehints_type:

\:py\:class\:\~mcp.server.mcpserver.server.MCPServer`\ \[:py:data:`~typing.Any`]`

returns:

The server, ready to run on any transport.

.. py:function:: compile_panel(source, syntax=’yaml’)

module:

scenet.mcp

Compile a document and return where the compiler put everything.

The result is the Panel Core for each panel: every figure’s position and scale, every balloon’s box and tail. Its notes say what the compiler did that the source did not literally ask for – a camera that pulled back so the cast would fit, a tail that bent around a face. A document that does not compile is an error carrying the same findings validate reports.

rtype:
sphinx_autodoc_typehints_type:

\:py\:class\:\~scenet.mcp.CompileReport``

returns:

Every panel, in reading order, with its notes and its Panel Core.

raises ToolError:

The document does not compile. The message lists every finding.

.. py:function:: get_spec(section=None)

module:

scenet.mcp

Read the Scenet language specification, one part at a time.

With no section: what Scenet is, the two ways to write a panel, the check-and-fix loop, the mistakes generators make most, and what every other part holds. Read it first. With a section: that part in full – language for every construct, comic-script for the script format, gallery for worked examples that are known to compile.

rtype:
sphinx_autodoc_typehints_type:

\:py\:class\:\str``

returns:

Markdown, exactly as it appears in the spec pack.

.. py:function:: list_puppets()

module:

scenet.mcp

List the characters a panel can cast, with every pose and expression each declares.

A cast member’s reference names one of these; its pose and expression must be names that puppet declares. Anything else fails validation.

rtype:
sphinx_autodoc_typehints_type:

\:py\:class\:\~scenet.mcp.PuppetCatalogue``

returns:

Every shipped puppet, by name, with its poses and expressions sorted.

.. py:function:: render_panel(source, syntax=’yaml’, live_text=False)

module:

scenet.mcp

Compile a document and return each panel as SVG.

The SVG is exactly what scenet build writes, one embedded image/svg+xml resource per panel, after a short text summary with the compiler’s notes. A document that does not compile is an error carrying the same findings validate reports.

rtype:
sphinx_autodoc_typehints_type:

\:py\:class\:\list`\ \[:py:class:`~mcp_types._types.TextContent` | :py:class:`~mcp_types._types.EmbeddedResource`]`

returns:

A summary, then one SVG resource per panel in reading order.

raises ToolError:

The document does not compile. The message lists every finding.

.. py:function:: serve(transport, **options)

module:

scenet.mcp

Run the server until the client disconnects.

type transport:
sphinx_autodoc_typehints_type:

\:py\:data\:\~typing.Literal`\ \[``’stdio’``, ``’streamable-http’``]`

param transport:

stdio for a local client that launches the server itself, or streamable-http to listen for remote ones.

type **options:
sphinx_autodoc_typehints_type:

\:py\:data\:\~typing.Any``

param **options:

Passed to the transport: host and port for Streamable HTTP.

rtype:
sphinx_autodoc_typehints_type:

\:py\:obj\:\None``

scenet.spec_pack#

.. py:module:: scenet.spec_pack

The spec pack: the whole language in one file, for a model to read.

Nothing tells a model how to write Scenet, and nothing will from training – a niche language is not in anybody’s weights, and pretending otherwise wastes effort. What works for every new library is the same thing: one self-contained document a model can be handed. This is that document, generated by scripts/build_spec.py from the language reference, the shot-type table, the comic-script guide, the puppet library, the diagnostic rule catalogue, the published JSON Schema and the whole gallery.

It ships inside the package because the MCP server’s get_spec tool serves it, and an installed wheel cannot read docs/. A test regenerates it and compares, so the copy here cannot go stale.

The pack is split into named parts, each opened by a marker comment, so a client that can ask for one part at a time – a tool call, an agent skill – need not pay for the other seven:

   <!-- scenet-spec:part=language -->

.. py:data:: MARKER

module:

scenet.spec_pack

value:

‘’

The comment that opens each part. One per part, on a line of its own.

.. py:data:: SPEC_PARTS

module:

scenet.spec_pack

type:

dict[str, str]

value:

{‘characters’: ‘The shipped puppets, with every pose and expression each declares’, ‘comic-script’: ‘The comic-script format: PANEL 1, @shot:, character cues, dialogue’, ‘diagnostics’: ‘Every rule scenet check reports, and what to do about each’, ‘gallery’: ‘Every gallery example in full, each known to compile’, ‘language’: ‘The language reference: every block and key of a panel or scene document’, ‘preamble’: ‘Start here: what Scenet is, the two ways to write a panel, the check-and-fix loop, and the mistakes generators make most’, ‘schema’: ‘The JSON Schema for a single-panel YAML document’, ‘shot-types’: ‘Normative: what each camera shot frames, in head-heights’}

Every part of the pack, in order, with what it is for. The descriptions are what a model sees when choosing which part to read, so they say what is in each one.

.. py:function:: part(name)

module:

scenet.spec_pack

One part of the pack, without its marker.

type name:
sphinx_autodoc_typehints_type:

\:py\:class\:\str``

param name:

A key of :data:SPEC_PARTS <scenet.spec_pack.SPEC_PARTS>.

rtype:
sphinx_autodoc_typehints_type:

\:py\:class\:\str``

returns:

The part’s Markdown, from the line after its marker up to the next marker.

raises KeyError:

There is no part by that name. The message lists the ones there are.

.. admonition:: Example

from scenet.spec_pack import part part(“comic-script”).lstrip().startswith(”# Write a panel as a comic script”) True

.. py:function:: spec_pack_text()

module:

scenet.spec_pack

The whole pack, exactly as published at /scenet-spec.md on the documentation site.

rtype:
sphinx_autodoc_typehints_type:

\:py\:class\:\str``

returns:

The Markdown text, read once and cached.

.. admonition:: Example

from scenet.spec_pack import spec_pack_text “” in spec_pack_text() True