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

TypeKindDescription
RequestrecordOne tool call taken off a connection and waiting for the frame.
ServerrecordA listening MCP endpoint, which answers only while something calls poll.
TooltypeOne registered tool and its handler.
ToolHandlertypeThe decoded arguments a tool receives and the structured content it returns.

Functions

FunctionKindDescription
bindfunctionNames the world and application the built-in tools act on.
bindLogFilefunctionNames the file getlogs reads.
crashedfunctionReturns the recorded crash text.
destroyfunctionReleases the server.
dispatchfunctionRuns one JSON-RPC request in process and returns the response text.
hasBuiltInToolsfunctionReports whether the built-in tools were registered when this module loaded.
listenfunctionStarts the server on port.
listToolsfunctionReturns every registered tool in registration order.
pollfunctionAnswers at most one tool call.
registerfunctionRegisters a tool, replacing any tool already holding its name.
resetSandboxfunctionForgets everything a previous runlua call stashed.
setCrashedfunctionRecords a crash, after which every world-touching tool reports it.

Types#

Requestrecord#

record Request
    name: string
    arguments: string
end

One tool call taken off a connection and waiting for the frame.

Fields

name#
name: string

Read-only. Names the registered tool the agent called.

arguments#
arguments: string

Read-only. Carries the call's arguments as the JSON object text the agent sent, which the frame decodes.

Serverrecord#

record Server
    port: integer
end

A listening MCP endpoint, which answers only while something calls poll.

Fields

port#
port: integer

Read-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?): nil

Names 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

NameTypeDescription
worldecs.World?

the world the world tools read and write, or nil to unbind

appbindings.Application?

the application the lifecycle tools act on, or nil to unbind

Returns

TypeDescription
nil

bindLogFilefunction#

function bindLogFile(path: string?): nil

Names 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

NameTypeDescription
pathstring?

the log file path, or nil to report that there is none

Returns

TypeDescription
nil

crashedfunction#

function crashed(): string?

Returns the recorded crash text.

Returns

TypeDescription
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): nil

Releases 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

NameTypeDescription
exclusive selfServer

the server to stop

Returns

TypeDescription
nil

dispatchfunction#

function dispatch(text: string): string

Runs 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

NameTypeDescription
textstring

one JSON-RPC request object, which may not be a batch

Returns

TypeDescription
string

the response as JSON text, which reports bad input in band rather than by raising

hasBuiltInToolsfunction#

function hasBuiltInTools(): boolean

Reports whether the built-in tools were registered when this module loaded.

Returns

TypeDescription
boolean

true, because requiring the module registers them

listenfunction#

function listen(port: integer?): Server?, string?

Starts the server on port.

Arguments

NameTypeDescription
portinteger?

the TCP port to bind, 7100 when nil, or zero to let the platform choose one and report it as Server.port

Returns

TypeDescription
Server?

the listening server, which answers nothing until something calls poll, or nil when the port could not be bound

string?

why it could not bind, when unsuccessful

listToolsfunction#

function listTools(): {Tool}

Returns every registered tool in registration order.

Returns

TypeDescription
{Tool}

a fresh list the caller owns, holding the registered declarations themselves rather than copies

pollfunction#

function poll(exclusive self: Server, parked: boolean?): boolean

Answers 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

NameTypeDescription
exclusive selfServer

the running server

parkedboolean?

whether a lifecycle operation is suspended; only whenParked tools may run then.

Returns

TypeDescription
boolean

whether one tool request was answered.

registerfunction#

function register(tool: Tool): nil

Registers 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

NameTypeDescription
toolTool

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

TypeDescription
nil

Raises

  • when the declaration carries no name or no handler

resetSandboxfunction#

function resetSandbox(): nil

Forgets 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

TypeDescription
nil

setCrashedfunction#

function setCrashed(traceback: string?): nil

Records 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

NameTypeDescription
tracebackstring?

the failure text, handed back verbatim by crashed and sent as the whole body of the error a blocked tool answers with, or nil to clear the crashed state

Returns

TypeDescription
nil