tecs.mcp
The MCP debug server, its tool registry, and the tools every build exposes.
Serving a session#
listen binds a loopback Streamable HTTP endpoint at http://127.0.0.1:<port>/mcp. An application configured with mcpPort owns the listener and tecs.host polls it once per host turn, outside world updates, including after a crash or while gameplay is suspended. For a standalone server, call poll outside the world update scope and destroy at teardown. Each poll answers at most one tool request; handlers must answer promptly.
While a lifecycle operation is parked, ping, context and get_logs remain available. Other tools report that the operation must complete first. A custom tool may opt in with whenParked = true only if it never touches the in-progress world. readOnly alone grants no such permission.
local server = assert(tecs.mcp.listen(7100))
tecs.mcp.bind(app.world, app)
-- once per frame
tecs.mcp.poll(server)Registering a tool#
local placed = world:newQuery({include = {tecs.Transform2D}})
mcp.register({
name = "placed_count",
description = "Count entities that carry a Transform2D",
inputSchema = {type = "object", properties = {}},
readOnly = true,
handler = function(_arguments: {[string]: any}): {[string]: any}?
local total = 0
for _, length in placed:iter() do
total = total + (length as integer)
end
return {count = total}
end
})Registration is what publishes the listing, so mutating a registered table in place does not change what a connected agent is told.
Inspecting a world#
Start with components_info, query and info. Use modify for a partial component update and set for a complete value or a missing component. systems_list and states_info report what the world is running. Prefer these structured tools to run_lua; the Lua sandbox limits accidents but does not create a security boundary.
After a crash#
setCrashed records a guarded gameplay failure. The server keeps polling after one: ping, context, get_logs, send_event and clear_crash remain available, and every tool that touches the world reports the stored traceback instead of running.
Subsystem tools#
Archetypes, resources, materials, camera, layers, render configuration and physics have structured inspection tools. snapshot_save and snapshot_load round-trip committed worlds, including binary physics state. audio and reload_sound use the world's installed mixer. profile_start and profile_stop use Nupp's process sampler; stop writes collapsed stacks under the writable root.
help, describe and capabilities enumerate the live registry. They and the sampler remain available while parked or crashed. The server does not provide GPU readback, shader or image reload, watcher orchestration, or physics debug overlay tools. Snapshot files use TECS framing over Nupp's binary codec and refuse unsupported framing or physics encodings.
Module contents
Types
| Type | Kind | Description |
|---|---|---|
Request | record | One tool call taken off a connection and waiting for the frame. |
Server | record | A listening MCP endpoint, which answers only while something calls poll. |
Tool | type | One registered tool and its handler. |
ToolHandler | type | The decoded arguments a tool receives and the structured content it returns. |
Functions
| Function | Kind | Description |
|---|---|---|
bind | function | Names the world and application the built-in tools act on. |
bindLogFile | function | Names the file getlogs reads. |
crashed | function | Returns the recorded crash text. |
destroy | function | Releases the server. |
dispatch | function | Runs one JSON-RPC request in process and returns the response text. |
hasBuiltInTools | function | Reports whether the built-in tools were registered when this module loaded. |
listen | function | Starts the server on port. |
listTools | function | Returns every registered tool in registration order. |
poll | function | Answers at most one tool call. |
register | function | Registers a tool, replacing any tool already holding its name. |
resetSandbox | function | Forgets everything a previous runlua call stashed. |
setCrashed | function | Records a crash, after which every world-touching tool reports it. |
Types#
Requestrecord#
One tool call taken off a connection and waiting for the frame.
Fields
arguments#
arguments: stringRead-only. Carries the call's arguments as the JSON object text the agent sent, which the frame decodes.
Serverrecord#
record Server
port: integer
endA listening MCP endpoint, which answers only while something calls poll.
Fields
port#
port: integerRead-only. Reports the bound TCP port, which is the one listen was given unless it was given zero.
Tooltype#
type Tool = {
name: string,
description: string?,
inputSchema: {[string]: any}?,
outputSchema: {[string]: any}?,
handler: ToolHandler,
readOnly: boolean?,
destructive: boolean?,
whenCrashed: boolean?,
--- Allows a tool to run during a parked lifecycle operation without touching the
--- world.
whenParked: boolean?
}One registered tool and its handler.
ToolHandlertype#
type ToolHandler = function(arguments: {[string]: any}): {[string]: any}?The decoded arguments a tool receives and the structured content it returns.
Functions#
bindfunction#
function bind(world: ecs.World?, app: bindings.Application?): nilNames the world and application the built-in tools act on.
One of each. The MCP protocol has nowhere to carry them, so they are held in module-level slots and every tool that needs one fails with a message saying so until this has run.
Arguments
| Name | Type | Description |
|---|---|---|
world | ecs.World? | the world the world tools read and write, or nil to unbind |
app | bindings.Application? | the application the lifecycle tools act on, or nil to unbind |
Returns
| Type | Description |
|---|---|
nil |
bindLogFilefunction#
function bindLogFile(path: string?): nilNames the file get_logs reads.
Nupp's logging writes to a sink rather than to a path it can be asked for, so the host that configured a file sink is the only thing that knows where it went. Until something calls this, get_logs reports no path and no lines rather than guessing at one.
Arguments
| Name | Type | Description |
|---|---|---|
path | string? | the log file path, or nil to report that there is none |
Returns
| Type | Description |
|---|---|
nil |
crashedfunction#
function crashed(): string?Returns the recorded crash text.
Returns
| Type | Description |
|---|---|
string? | the traceback, or nil when nothing has recorded a crash; nothing here polls the game to confirm that it remains healthy |
destroyfunction#
function destroy(exclusive self: Server): nilReleases the server. Safe to call more than once.
One way: the port is given up and the server cannot be made to listen again. The tool registry is process-wide and is left untouched, so a later listen answers the same tools.
Arguments
| Name | Type | Description |
|---|---|---|
exclusive self | Server | the server to stop |
Returns
| Type | Description |
|---|---|
nil |
dispatchfunction#
function dispatch(text: string): stringRuns one JSON-RPC request in process and returns the response text.
The transport is not involved, so this is what a test drives and what a host embedding the protocol on a channel of its own can call. A tool handler runs on the calling thread, inside this call.
Arguments
| Name | Type | Description |
|---|---|---|
text | string | one JSON-RPC request object, which may not be a batch |
Returns
| Type | Description |
|---|---|
string | the response as JSON text, which reports bad input in band rather than by raising |
hasBuiltInToolsfunction#
function hasBuiltInTools(): booleanReports whether the built-in tools were registered when this module loaded.
Returns
| Type | Description |
|---|---|
boolean | true, because requiring the module registers them |
listenfunction#
function listen(port: integer?): Server?, string?Starts the server on port.
Arguments
| Name | Type | Description |
|---|---|---|
port | integer? | the TCP port to bind, 7100 when nil, or zero to let the platform choose one and report it as |
Returns
| Type | Description |
|---|---|
Server? | the listening server, which answers nothing until something calls |
string? | why it could not bind, when unsuccessful |
listToolsfunction#
function listTools(): {Tool}Returns every registered tool in registration order.
Returns
| Type | Description |
|---|---|
{Tool} | a fresh list the caller owns, holding the registered declarations themselves rather than copies |
pollfunction#
function poll(exclusive self: Server, parked: boolean?): booleanAnswers at most one tool call. Call once per host turn outside world updates.
Protocol-only traffic, initialize and tools/list included, is answered without running a tool and therefore does not make this report true.
Arguments
| Name | Type | Description |
|---|---|---|
exclusive self | Server | the running server |
parked | boolean? | whether a lifecycle operation is suspended; only |
Returns
| Type | Description |
|---|---|
boolean | whether one tool request was answered. |
registerfunction#
function register(tool: Tool): nilRegisters a tool, replacing any tool already holding its name.
A replacement keeps the name's original position, so the order a client sees is first-registration order rather than last.
Arguments
| Name | Type | Description |
|---|---|---|
tool | Tool | the declaration, stored by reference and never copied, so a later mutation changes what the server dispatches but not what a connected agent has been told |
Returns
| Type | Description |
|---|---|
nil |
Raises
when the declaration carries no name or no handler
resetSandboxfunction#
function resetSandbox(): nilForgets everything a previous run_lua call stashed.
The next run_lua compiles into a fresh environment. A chunk already compiled keeps the old one, so a reset makes a clean slate for new code rather than for code already in flight.
Returns
| Type | Description |
|---|---|
nil |
setCrashedfunction#
function setCrashed(traceback: string?): nilRecords a crash, after which every world-touching tool reports it.
The server outliving the game is the point. An agent debugging something up to the moment it broke should get the reason rather than a refused connection, and the tools that do not touch the world keep working so it can still read the log and ask what the build was.
Arguments
| Name | Type | Description |
|---|---|---|
traceback | string? | the failure text, handed back verbatim by |
Returns
| Type | Description |
|---|---|
nil |