trame-mcp
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@trame-mcpset the cone resolution to 12 and tell me the current opacity"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
trame-mcp
Add an MCP (Model Context Protocol) server to any trame application, so LLM clients (Claude Code, Claude Desktop, ...) can inspect and drive the app while human users watch it update live in the browser.
The MCP endpoint is served on the same port as the application itself — no second server, no second process.
Quick start
from trame.app import get_server
from trame_mcp import get_mcp
server = get_server()
mcp = get_mcp(server) # the MCP server attached to this trame server
server.state.resolution = 6
@mcp.tool
def set_resolution(resolution: int) -> str:
"""Set the number of facets of the cone."""
with server.state as state:
state.resolution = resolution
return f"Resolution set to {resolution}"
mcp.enabled = True # nothing is served until agent access is on
server.start()At startup the console shows how to connect:
MCP server available at http://localhost:8080/mcp
claude mcp add --transport http trame http://localhost:8080/mcpRelated MCP server: mcp-server
Declaring what the agent can use
The decorators follow trame's own split between plain functions and methods:
trame | trame-mcp | |
Plain functions: exposed on that server right away |
|
|
Methods: inert markers, bound when the object is built |
|
|
mcp is always the object get_mcp(server) returns: the MCP server of that
trame server, one per server like get_server(). The markers (tool,
resource, prompt, plus mcp_field for trame-dataclass fields) are
imported from trame_mcp; like trame.decorators, they need no server, so
libraries can use them freely. So do the base classes, which pair with
trame's: MCPApp / TrameApp, MCPComponent / TrameComponent and
MCPStateDataModel / StateDataModel.
What | Declared with | The agent gets |
Functions |
| a tool, resource or prompt each |
App and component methods |
| the same, optionally prefixed |
Plain trame state |
|
|
trame-dataclass models |
|
|
Declaring serves nothing: agent access stays off until mcp.enabled = True
(Declare, then go live). Every value an agent sends
is validated against the schema it was shown before any app code runs.
Functions
from typing import Annotated
from trame.app import get_server
from trame_mcp import get_mcp
server = get_server()
mcp = get_mcp(server)
server.state.resolution = 6
server.state.opacity = 1.0
@mcp.tool
def set_opacity(
opacity: Annotated[float, "0 = transparent, 1 = opaque", {"minimum": 0, "maximum": 1}],
) -> str:
"""Set the opacity of the cone."""
with server.state as state:
state.opacity = opacity
return f"Opacity set to {opacity}"
@mcp.tool(read_only=True) # advertised as safe: clients may skip confirmation
def get_settings() -> dict:
"""Current resolution and opacity."""
return {"resolution": server.state.resolution, "opacity": server.state.opacity}
@mcp.resource("app://help", mime_type="text/markdown")
def help_page() -> str:
"""How to use this application."""
return "# Cone viewer\nChange the resolution and the opacity of the cone."
@mcp.prompt
def smooth_cone(level: str = "very") -> str:
"""Ask the agent to smooth the cone."""
return f"Make the cone {level} smooth without making it transparent."The name comes from the function and the description from its docstring (override with
name=/description=).The input schema comes from the type hints;
Annotatedadds a description (a string) and JSON Schema constraints (a dict).str,dictandlistresults become text (dicts also as structured content); returntrame_mcp.Image(png_bytes)to send an image.Functions may be sync or
async; all run on trame's event loop, insidewith state:, so what they assign reaches browsers (and@state.changereactions run) when they return.A module that has no server at hand can mark its functions with
@toolinstead; the app then exposes them withmcp.expose(fn, ...).
Methods of app classes and components
from typing import Literal
from trame_mcp import MCPApp, MCPComponent, resource, tool
class Viewer(MCPComponent): # may ship in a library: it never turns MCP on
@tool
def set_background(self, color: Literal["black", "white", "gray"]) -> str:
"""Set the background color of the 3D view."""
with self.state:
self.state.background = color
return f"Background set to {color}"
class ConeApp(MCPApp):
def __init__(self, server=None):
super().__init__(server)
self.state.resolution = 6
self.viewer = Viewer(self.server, mcp_prefix="viewer_")
self.mcp.configure(name="cone-app", enabled=True)
@tool
def set_resolution(self, resolution: int) -> str:
"""Set the number of facets of the cone."""
with self.state:
self.state.resolution = resolution
return f"Resolution set to {resolution}"
@resource("app://settings", mime_type="application/json")
def settings(self) -> dict:
"""Current view settings."""
return {"resolution": self.state.resolution}
if __name__ == "__main__":
ConeApp().server.start()MCPApp and MCPComponent are trame's TrameApp and TrameComponent,
plus one thing: when built, they expose their own marked methods, the way
trame binds @change and @trigger. The agent sees set_resolution,
viewer_set_background and the app://settings resource. self.mcp is the
object get_mcp(self.server) returns; use it once super().__init__() has
run.
MCPMixindoes the same for an app that already derives from anotherTrameAppsubclass:class App(MCPMixin, TheirApp).Any other object with marked methods is exposed explicitly:
mcp.expose(obj, prefix="obj_").
Plain trame state
from trame.app import get_server
from trame_mcp import get_mcp
server = get_server()
server.state.resolution = 6
server.state.color = "red"
mcp = get_mcp(server)
mcp.state.expose(
"resolution",
description="Number of facets of the cone",
type=int,
schema={"minimum": 3, "maximum": 60},
)
mcp.state.expose("color", read_only=True, description="Cone color, a CSS color name")The agent reads exposed variables with state_get and assigns the writable
ones with state_update. Updates are all-or-nothing, checked against type
and schema, and applied inside with state:, so @state.change reactions
fire and browsers update. mcp.state.unexpose("color") withdraws a variable
at any time.
trame-dataclass models
Two declarations work together: models say which classes the agent may see, and which of their fields and methods; instances say where it starts. Everything reachable from an exposed instance through exposed fields is then addressable, including instances created later.
Models you own derive from MCPStateDataModel, the agent-visible
StateDataModel, and mark their fields and methods. This needs no server:
from trame_dataclass.v2 import ServerOnly, Sync
from trame_mcp import MCPStateDataModel, mcp_field, tool
class Person(MCPStateDataModel):
name = mcp_field(Sync(str, ""), "Full name", schema={"minLength": 1})
age = mcp_field(Sync(int, 0), "Age in years", schema={"minimum": 0})
notes = ServerOnly(str) # not marked: invisible to the agent
@tool
def celebrate_birthday(self) -> str:
"""Increment this person's age by one year."""
self.age += 1
return f"{self.name} is now {self.age}"
class AddressBook(
MCPStateDataModel, all_fields=True, all_tools=True, tools={"export_csv": {"hidden": True}}
):
title = Sync(str, "My contacts")
contacts = Sync(list[Person], list, has_dataclass=True)
def add_contact(self, name: str, age: int = 0) -> dict:
"""Create a contact and add it to the book."""
person = Person(self.server, name=name, age=age)
self.contacts = [*self.contacts, person]
return {"id": person._id}
def export_csv(self, path: str) -> None: # public, but hidden from the agent
...mcp_field(descriptor, description, read_only=, hidden=, schema=)opts a field in. Whether the agent may write it follows the field's mode (Syncwritable,ClientOnlyread-only; aServerOnlyfield needs an explicitread_only=), andschemaconstraints are enforced on every write.@toolopts a method in;@tool(read_only=True)keeps it callable on read-only instances.Class keywords set the options:
all_fields=True/all_tools=Trueopt in every field by its mode and every public method.fields=/tools=list names, or map them to the same options asmcp_field/@tool;{"hidden": True}excludes one.Subclasses inherit the options. Keywords on a subclass replace
all_fields/all_toolsand addfields/toolsentries.
The application then exposes, on its server, the models it doesn't own and the instances the agent starts from:
from trame.app import get_server
from trame_dataclass.v2 import StateDataModel, Sync
from trame_mcp import get_mcp
class Camera(StateDataModel): # stands in for a model from a library
position = Sync(list[float], lambda: [0.0, 0.0, 1.0])
focal_point = Sync(list[float], lambda: [0.0, 0.0, 0.0])
cache_key = Sync(str, "")
def reset(self) -> None:
"""Look at the origin from the default position."""
self.position = [0.0, 0.0, 1.0]
self.focal_point = [0.0, 0.0, 0.0]
server = get_server()
mcp = get_mcp(server)
mcp.dataclass.expose_model(
Camera,
fields={
"position": {"description": "[x, y, z]", "schema": {"minItems": 3, "maxItems": 3}},
"focal_point": {"read_only": True},
},
tools=["reset"],
)
# Person and AddressBook as defined above
book = AddressBook(server, contacts=[Person(server, name="Ada", age=36)])
camera = Camera(server)
presets = {"front": Camera(server), "top": Camera(server, position=[0.0, 1.0, 0.0])}
mcp.dataclass.expose_instance(book, name="address_book")
mcp.dataclass.expose_instance(lambda: camera, name="camera") # looked up on every call
mcp.dataclass.expose_instance(presets, name="presets", read_only=True)expose_model()takes the same options as theMCPStateDataModelclass keywords, once per class; a plain list works too (fields=["position", "focal_point"]). It also overrides the options of anMCPStateDataModelsubclass, so the app has the final word.An instance source is an instance, a list or dict of instances, or a function evaluated on every call, which keeps following a variable or attribute that gets reassigned.
read_only=Trueallows reading only, plus@tool(read_only=True)methods.mcp.dataclass.unexpose_instance(name)withdraws an entry point.
The agent then works with four generic tools:
dataclass_schemas() # models, fields, methods, entry points
dataclass_get("address_book", depth=2) # values, nested instances inlined
dataclass_call("address_book", "add_contact", {"name": "Grace", "age": 36})
dataclass_update("<person id>", {"age": 37}) # validated, all-or-nothingdataclass_update and dataclass_call return once the instance's
@watchers have run, so what they derive is in the agent's next read.
What the agent reads
Everything the agent knows about the app comes from what you declare:
On connection, the server sends instructions: your
mcp.instructionstext, if any, then a generated overview. It says the app is live and shared with the people using it, and where to start (data entry points, state).Tools, resources and prompts are described by their docstrings. Parameters, prompt arguments included, take descriptions and constraints from
Annotated[T, "description", {...}].trame-dataclass models are described by
dataclass_schemas(): class docstrings,mcp_fielddescriptions and schemas, method docstrings and parameters. A subclass lists only what it adds or changes, under"extends": "BaseModel".
Docstrings reach the agent verbatim. When one is written for developers, give
the agent its own text with description=: on @mcp.tool / @tool, as an
MCPStateDataModel class keyword or an expose_model() option, and in
fields= / tools= entries. A model's description, like its docstring, is
not inherited by its subclasses.
Declare, then go live
Everything above only declares what an agent could use. None of it is reachable until agent access is turned on, so an app, or a library it uses, can carry its whole MCP surface and still ship with it off.
One switch goes live:
mcp.enabled(defaultFalse) covers every agent channel, the HTTP endpoint and the in-process chat (LLMAdapter) alike. Set it in code, flip it at runtime, or let the end user decide with--mcp/--no-mcp.The command line has the last word at launch: before the server starts, values set in code are defaults that
--mcp/--no-mcp(and--mcp-token) override. Once it is running, every assignment applies.
from trame.app import get_server
from trame_mcp import get_mcp
server = get_server()
mcp = get_mcp(server)
mcp.enabled = True # a default: launching with --no-mcp still keeps it off
@server.state.change("agent_access") # e.g. bound to a checkbox in the UI
def on_agent_access(agent_access, **_):
mcp.enabled = agent_access
|
| |
HTTP endpoint | answers 503 | served |
HTTP with | no route | no route |
In-process | calls raise | served |
http=False is the structural choice, fixed once the server starts: no route
is ever mounted, for an app whose embedded chat is the only agent. Otherwise
the route is mounted even while access is off, so it can be switched on later;
the connection banner prints when access first goes live.
Options
get_mcp(server, **options) and mcp.configure(**options) set several
options at once; each is also an attribute (mcp.name = "cone-app").
Option | Default | Meaning |
|
| agent access switch; |
| the trame server name | server name reported to MCP clients |
|
| your guidance for MCP clients, sent before the generated overview |
|
| bearer token required on the endpoint; |
|
| extra browser origins allowed to call the endpoint |
|
| mount the HTTP endpoint; |
|
| endpoint path on the trame web server |
|
| run tool calls one at a time |
http and path are fixed once the server starts; the others can change at
any time.
Endpoint security
Browser pages from other sites cannot reach the endpoint (Origin, Host and Content-Type checks), so the default tokenless setup is safe on localhost.
token=/--mcp-token/TRAME_MCP_TOKENrequire a bearer token; when the server listens beyond localhost without one, a token is generated and printed with the connection banner.
Using with trame-llm
The same mcp can feed trame-llm's
embedded chat widget, in-process, through LLMAdapter:
from trame.app import get_server
from trame_llm.core import LLMClient
from trame_mcp import LLMAdapter, get_mcp
mcp = get_mcp(get_server())
llm_client = LLMClient()
llm_client.register_mcp(LLMAdapter(mcp)) # after everything is declaredmcp.enabled applies to the chat too. While it is off, the chat still lists
the tools (trame-llm reads them once at startup), but every call fails with
"MCP access is currently disabled by the application". For a chat-only
application that should not accept outside MCP connections, pass
http=False to get_mcp() (or self.mcp.configure(http=False)).
Examples
examples/00_quickstart.py— a function tool and plain state.examples/01_class_based.py—MCPApp; starts with agent access off and toggles it from the UI.examples/02_address_book.py— trame-dataclass models and instances.examples/03_wasm_quadrants.py— nested models, inheritance,all_fields/all_tools, VTK scenes.
See DESIGN.md for the full design document.
License
This library is OpenSource and follows the Apache Software License.
This server cannot be deployed
Maintenance
Related MCP Connectors
Enable secure connectivity between Sentry issues and debugging data, and LLM clients, using a Model Context Protocol (MCP) server.
Model Context Protocol server for the Apideck Unified API. Connect any MCP-compatible agent framework to 100+ accounting systems, HRIS platforms, file storage providers, and more through one integration. More information https://www.apideck.com/mcp-server
MCP server for your apps' tools and custom tools, plus hosted AI agents and approval-gated workflows
MCP server for secureFlows: token-free URL builders and integration-linting tools for AI agents.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceExposes FastAPI endpoints as Model Context Protocol (MCP) tools while preserving existing authentication, schemas, and documentation. It enables seamless integration of FastAPI services into MCP ecosystems using a native ASGI transport layer.MIT
- FlicenseNot gradedqualityDmaintenanceSets up a Model Context Protocol (MCP) server with FastMCP, allowing LLMs to use prompts, tools, and resources.-
- AlicenseNot gradedqualityCmaintenanceMounts an MCP server onto FastAPI to let AI agents inspect routes, endpoints, and schemas at runtime.50 PyPIMIT

@absolutejs/mcpofficial
AlicenseNot gradedqualityBmaintenanceEnables serving a remote Model Context Protocol endpoint by exposing tools, prompts, and resources with customizable authorization and hooks, allowing AI models to interact with your application through a standardized JSON-RPC interface.673 npmBusiness Source 1.1