
# 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 <const> = 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) | <span class="tealdoc-kind-badge tealdoc-kind-record">record</span> | Represents a listening MCP endpoint. |
| [`Tool`](/modules/io/mcp/#tecs.io.mcp.Tool) | <span class="tealdoc-kind-badge tealdoc-kind-record">record</span> | Describes one registered tool and its handler. |
| [`ToolHandler`](/modules/io/mcp/#tecs.io.mcp.ToolHandler) | <span class="tealdoc-kind-badge tealdoc-kind-type">type</span> | 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) | <span class="tealdoc-kind-badge tealdoc-kind-static">Static</span> | Returns the crash traceback, or nil while the game is healthy. |
| [`dispatch`](/modules/io/mcp/#tecs.io.mcp.dispatch) | <span class="tealdoc-kind-badge tealdoc-kind-static">Static</span> | Runs one JSON-RPC request and returns the response text. |
| [`listen`](/modules/io/mcp/#tecs.io.mcp.listen) | <span class="tealdoc-kind-badge tealdoc-kind-static">Static</span> | Starts the server on port. |
| [`register`](/modules/io/mcp/#tecs.io.mcp.register) | <span class="tealdoc-kind-badge tealdoc-kind-static">Static</span> | Registers a tool. |
| [`setCrashed`](/modules/io/mcp/#tecs.io.mcp.setCrashed) | <span class="tealdoc-kind-badge tealdoc-kind-static">Static</span> | Records a crash. |
| [`tools`](/modules/io/mcp/#tecs.io.mcp.tools) | <span class="tealdoc-kind-badge tealdoc-kind-static">Static</span> | Returns every registered tool in registration order. |

## Types

<a id="tecs.io.mcp.Server"></a>
### tecs.io.mcp.Server <span class="tealdoc-kind-badge tealdoc-kind-record">record</span>

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
```

<a id="tecs.io.mcp.Server.port"></a>
#### tecs.io.mcp.Server.port <span class="tealdoc-kind-badge tealdoc-kind-field">field</span>

Read-only. This is the bound TCP port, which is 7100 when `listen`
was given none.


```teal
tecs.io.mcp.Server.port: integer
```

<a id="tecs.io.mcp.Server.destroy"></a>
#### tecs.io.mcp.Server:destroy <span class="tealdoc-kind-badge tealdoc-kind-instance">Instance</span>

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.

<a id="tecs.io.mcp.Server.poll"></a>
#### tecs.io.mcp.Server:poll <span class="tealdoc-kind-badge tealdoc-kind-instance">Instance</span>

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. |

<a id="tecs.io.mcp.Tool"></a>
### tecs.io.mcp.Tool <span class="tealdoc-kind-badge tealdoc-kind-record">record</span>

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
```

<a id="tecs.io.mcp.Tool.name"></a>
#### tecs.io.mcp.Tool.name <span class="tealdoc-kind-badge tealdoc-kind-field">field</span>

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
```

<a id="tecs.io.mcp.Tool.description"></a>
#### tecs.io.mcp.Tool.description <span class="tealdoc-kind-badge tealdoc-kind-field">field</span>

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
```

<a id="tecs.io.mcp.Tool.inputSchema"></a>
#### tecs.io.mcp.Tool.inputSchema <span class="tealdoc-kind-badge tealdoc-kind-field">field</span>

Caller-writable. This JSON Schema describes the arguments and is
sent verbatim in the tool list.


```teal
tecs.io.mcp.Tool.inputSchema: {string: any}
```

<a id="tecs.io.mcp.Tool.handler"></a>
#### tecs.io.mcp.Tool.handler <span class="tealdoc-kind-badge tealdoc-kind-field">field</span>

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
```

<a id="tecs.io.mcp.Tool.readOnly"></a>
#### tecs.io.mcp.Tool.readOnly <span class="tealdoc-kind-badge tealdoc-kind-field">field</span>

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
```

<a id="tecs.io.mcp.Tool.destructive"></a>
#### tecs.io.mcp.Tool.destructive <span class="tealdoc-kind-badge tealdoc-kind-field">field</span>

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
```

<a id="tecs.io.mcp.Tool.whenCrashed"></a>
#### tecs.io.mcp.Tool.whenCrashed <span class="tealdoc-kind-badge tealdoc-kind-field">field</span>

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
```

<a id="tecs.io.mcp.ToolHandler"></a>
### tecs.io.mcp.ToolHandler <span class="tealdoc-kind-badge tealdoc-kind-type">type</span>

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

<a id="tecs.io.mcp.crashed"></a>
### tecs.io.mcp.crashed <span class="tealdoc-kind-badge tealdoc-kind-static">Static</span>

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. |

<a id="tecs.io.mcp.dispatch"></a>
### tecs.io.mcp.dispatch <span class="tealdoc-kind-badge tealdoc-kind-static">Static</span>

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. |

<a id="tecs.io.mcp.listen"></a>
### tecs.io.mcp.listen <span class="tealdoc-kind-badge tealdoc-kind-static">Static</span>

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`. |

<a id="tecs.io.mcp.register"></a>
### tecs.io.mcp.register <span class="tealdoc-kind-badge tealdoc-kind-static">Static</span>

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.

<a id="tecs.io.mcp.setCrashed"></a>
### tecs.io.mcp.setCrashed <span class="tealdoc-kind-badge tealdoc-kind-static">Static</span>

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.

<a id="tecs.io.mcp.tools"></a>
### tecs.io.mcp.tools <span class="tealdoc-kind-badge tealdoc-kind-static">Static</span>

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. |