# tecs.io.mcp MCP debug server and tool registry. ## Server lifecycle Set `Application.Config.mcpPort` for the built-in server. A custom host can call `listen`, call `Server:poll` once per frame, and call `Server:destroy` at teardown. The listener accepts loopback connections only. Each call to `poll` runs at most one queued handler. The handler sees a committed world, but it must return quickly because the frame cannot end until it does. ## Custom tools ```teal local placed = world:newQuery({include = {tecs.Transform2D}}) tecs.io.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} return {count = placed:count()} end, }) ``` The registry produces both the advertised tool list and dispatch table. Tool handlers can use the world captured when the game registers them. ## World inspection Start with `components_info`, `query`, and `info`. Use `modify` for a partial component update and `set` for a complete value or a missing component. `modify` skips entities without the component and marks changed columns dirty. Prefer these structured tools to `run_lua`; the Lua sandbox limits accidents but does not create a security boundary. ## Crash access The server keeps polling after an uncaught gameplay error. `ping`, `context`, `get_logs`, and `send_event` remain available; tools that touch the world report the stored traceback instead. ## Module contents ### Types | Type | Kind | Description | | --- | --- | --- | | [`Server`](/modules/io/mcp/#tecs.io.mcp.Server) | record | Represents a listening MCP endpoint. | | [`Tool`](/modules/io/mcp/#tecs.io.mcp.Tool) | record | Describes one registered tool and its handler. | | [`ToolHandler`](/modules/io/mcp/#tecs.io.mcp.ToolHandler) | type | Defines the decoded arguments a tool receives and the structured content it returns. | ### Functions | Function | Kind | Description | | --- | --- | --- | | [`crashed`](/modules/io/mcp/#tecs.io.mcp.crashed) | Static | Returns the crash traceback, or nil while the game is healthy. | | [`dispatch`](/modules/io/mcp/#tecs.io.mcp.dispatch) | Static | Runs one JSON-RPC request and returns the response text. | | [`listen`](/modules/io/mcp/#tecs.io.mcp.listen) | Static | Starts the server on port. | | [`register`](/modules/io/mcp/#tecs.io.mcp.register) | Static | Registers a tool. | | [`setCrashed`](/modules/io/mcp/#tecs.io.mcp.setCrashed) | Static | Records a crash. | | [`tools`](/modules/io/mcp/#tecs.io.mcp.tools) | Static | Returns every registered tool in registration order. | ## Types ### tecs.io.mcp.Server record Represents a listening MCP endpoint. It answers only while something calls `poll`. ```teal record tecs.io.mcp.Server port: integer destroy: function(self) poll: function(self): boolean end ``` #### tecs.io.mcp.Server.port field Read-only. This is the bound TCP port, which is 7100 when `listen` was given none. ```teal tecs.io.mcp.Server.port: integer ``` #### tecs.io.mcp.Server:destroy Instance 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 module-wide and is left untouched, so a later `listen` answers the same tools. ```teal function tecs.io.mcp.Server.destroy(self) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Server` | | ##### Returns None. #### tecs.io.mcp.Server:poll Instance Answers at most one tool call. Call once per frame. ```teal function tecs.io.mcp.Server.poll(self): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Server` | | ##### Returns | Type | Description | | --- | --- | | `boolean` | True when one tool handler ran. Protocol-only traffic does not count. False when no tool call is waiting or after destruction. | ### tecs.io.mcp.Tool record Describes one registered tool and its handler. ```teal record tecs.io.mcp.Tool name: string description: string inputSchema: {string: any} handler: ToolHandler readOnly: boolean destructive: boolean whenCrashed: boolean end ``` #### tecs.io.mcp.Tool.name field Caller-writable. This is the name an agent calls and the registry key. Established MCP tool names follow the protocol ecosystem's spelling, including `run_lua` and `send_event`, so they use snake_case where the rest of this tree uses camelCase. This Do not rename this compatibility surface. Required. ```teal tecs.io.mcp.Tool.name: string ``` #### tecs.io.mcp.Tool.description field Caller-writable. This is one line shown to the agent alongside the name. Optional; nil is sent as an empty string rather than omitted. ```teal tecs.io.mcp.Tool.description: string ``` #### tecs.io.mcp.Tool.inputSchema field Caller-writable. This JSON Schema describes the arguments and is sent verbatim in the tool list. ```teal tecs.io.mcp.Tool.inputSchema: {string: any} ``` #### tecs.io.mcp.Tool.handler field Caller-writable. This handler runs synchronously inside `poll`. Required; its absence raises at registration rather than at call time. ```teal tecs.io.mcp.Tool.handler: ToolHandler ``` #### tecs.io.mcp.Tool.readOnly field Caller-writable. This declares whether the tool only reads state so an agent can judge a call before making it. ```teal tecs.io.mcp.Tool.readOnly: boolean ``` #### tecs.io.mcp.Tool.destructive field Caller-writable. This declares whether a call changes state that a caller would not want changed without asking. Optional, false when nil. Advisory, like `readOnly`: the server reports both values to the agent and enforces neither, so a tool declared read-only may still write. ```teal tecs.io.mcp.Tool.destructive: boolean ``` #### tecs.io.mcp.Tool.whenCrashed field Caller-writable. This allows the tool to remain callable after the game has crashed. False by default, because a crashed world cannot answer anything about itself, and a tool that read it would return nonsense rather than an error. ```teal tecs.io.mcp.Tool.whenCrashed: boolean ``` ### tecs.io.mcp.ToolHandler type Defines the decoded arguments a tool receives and the structured content it returns. ```teal type tecs.io.mcp.ToolHandler = function({string: any}): {string: any} ``` ## Functions ### tecs.io.mcp.crashed Static Returns the crash traceback, or nil while the game is healthy. ```teal function tecs.io.mcp.crashed(): string ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | `string` | nil means no code has recorded a crash. Nothing here polls the game to confirm that it remains healthy. | ### tecs.io.mcp.dispatch Static Runs one JSON-RPC request and returns the response text. Lets tests exercise the protocol without a socket, which covers most of the failure surface. The three methods answered, `initialize`, `tools/list` and `tools/call`, use the names from the MCP specification, which this tree must not rename. Anything else is a method-not-found error. When a handler raises, the server sets `isError` for the agent and continues. A handler may return nil, which produces an empty `structuredContent`. ```teal function tecs.io.mcp.dispatch(text: string): string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `text` | `string` | One JSON-RPC request object. The server does not support a batch, which decodes as a JSON array with no `method` and produces an invalid-request error rather than dispatching each element. | #### Returns | Type | Description | | --- | --- | | `string` | Always a response as JSON text, never nil, and never a raise, since the response reports bad input in-band. The server still answers a request with no `id`, omitting the `id` field from the response. | ### tecs.io.mcp.listen Static Starts the server on `port`. ```teal function tecs.io.mcp.listen(port: integer): mcp.Server ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `port` | `integer` | Defaults to 7100. A port already in use raises rather than selecting another port, since a debugger could not find a moved server. | #### Returns | Type | Description | | --- | --- | | [`mcp.Server`](/modules/io/mcp/#tecs.io.mcp.Server) | Listening, but answering nothing until something calls `poll`. | ### tecs.io.mcp.register Static Registers a tool. Re-registering a name replaces it. Replacing keeps the name's original position in the list, so the order a client sees is first-registration order rather than last. ```teal function tecs.io.mcp.register(tool: mcp.Tool) ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `tool` | [`mcp.Tool`](/modules/io/mcp/#tecs.io.mcp.Tool) | Stored by reference and never copied, so mutating the table afterwards changes what the server dispatches and reports. `name` and The call requires `name` and `handler` and raises when either is absent; the rest may be nil. | #### Returns None. ### tecs.io.mcp.setCrashed Static Records a crash. Every subsequent world-touching call reports it. ```teal function tecs.io.mcp.setCrashed(traceback: string) ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `traceback` | `string` | Stored as given and handed back verbatim by `crashed`, and sent as the whole body of the error a blocked tool answers with. Passing nil clears the crashed state and lets every tool run again. | #### Returns None. ### tecs.io.mcp.tools Static Returns every registered tool in registration order. ```teal function tecs.io.mcp.tools(): {mcp.Tool} ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | `{`[`mcp.Tool`](/modules/io/mcp/#tecs.io.mcp.Tool)`}` | A fresh list each call, so the caller may keep it. The [`Tool`](/modules/io/mcp/#tecs.io.mcp.Tool) values in it are the registered tables themselves rather than copies, so writing to one writes to the registry. |