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 itlist_puppets– the characters it may cast, with their poses and expressionsvalidate– every finding at once, shaped rather than dumped as raw SARIFcompile– the Panel Core: where the compiler put everything, and whyrender– the SVG, byte-identical to whatscenet buildwrites
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.BaseModelEvery 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.BaseModelOne 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.BaseModelOne 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.BaseModelA character a cast member’s
referencecan 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.BaseModelThe 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.BaseModelEverything 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
validatereports.- 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 –
languagefor every construct,comic-scriptfor the script format,galleryfor 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
referencenames one of these; itsposeandexpressionmust 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 buildwrites, one embeddedimage/svg+xmlresource per panel, after a short text summary with the compiler’s notes. A document that does not compile is an error carrying the same findingsvalidatereports.- 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:
stdiofor a local client that launches the server itself, orstreamable-httpto listen for remote ones.- type **options:
- sphinx_autodoc_typehints_type:
\:py\:data\:\~typing.Any``
- param **options:
Passed to the transport:
hostandportfor 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 checkreports, 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.mdon 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