# Use an MCP server's tools

> Register an MCP server with the runtime, and call its tools from agents and flows like your own.

This guide registers an MCP server with the runtime, so your agents and flows call its tools like your own.

An MCP server you run, or one a vendor runs, can give your agents its tools. Register its endpoint with the runtime. Kindgi lists the server's tools and registers each one as a tool of your tenant, named `<endpoint id>.<tool name>`. Agents and flow steps then call it like a tool of your pack.

> **Note**
>
> `kindgi mcp` is a different thing: it adds MCP servers to your coding agent's `.mcp.json`. The runtime's MCP endpoints are registered through the API, as this guide shows.

## Before you start

- **A runtime,** such as `kindgi dev`, with Kindgi 0.1's CLI.
- **`KINDGI_API_URL` and `KINDGI_API_TOKEN`** set in your shell: your runtime's URL and its API token. `kindgi dev` prints both.
- **`uv`,** to run the example server.
- **An agent and a model that calls tools,** to call the server's tools. This guide uses `my-pack.order-desk`.

## A server to try

An MCP server that speaks Streamable HTTP works. Ask your coding agent: "I want a small MCP server to try, with a tool that checks stock levels." For reference, this one, written with the Python MCP SDK, version 1.x, has two tools:

`server.py`

```python
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("inventory", host="127.0.0.1", port=8791)

STOCK = {"SKU-1": 12, "SKU-2": 0}


@mcp.tool()
def stock_level(sku: str) -> dict:
    """Returns how many units of a SKU are in stock."""
    return {"sku": sku, "units": STOCK.get(sku, 0)}


@mcp.tool()
def reserve(sku: str, units: int) -> dict:
    """Reserves units of a SKU for an order."""
    return {"sku": sku, "reserved": units}


if __name__ == "__main__":
    mcp.run(transport="streamable-http")
```

Run it from the folder that holds `server.py`:

```sh
uv init --bare --no-workspace
uv add 'mcp<2'
uv run python server.py
```

- **`uv init --bare --no-workspace`** makes the folder a project of its own, with no sample files.
- **`uv add 'mcp<2'`** adds the MCP SDK, version 1.x.

`uv init` prints `Initialized project` with the folder's name, and `uv add` prints the packages it installed. The server then prints:

```text
INFO:     Started server process [52998]
INFO:     Waiting for application startup.
StreamableHTTP session manager started
INFO:     Application startup complete.
INFO:     Uvicorn running on http://127.0.0.1:8791 (Press CTRL+C to quit)
```

It keeps running, and serves at `http://localhost:8791/mcp`. Under `kindgi dev`, the runtime reaches your machine's `localhost`.

## Register the endpoint

Registering the endpoint makes its tools your tenant's. Register it from your app's code. Ask your coding agent: "Register my inventory MCP server with the Kindgi runtime, so my agents can use its tools." For reference, the code looks like this. This guide's endpoint id is `inventory`:

- **TypeScript**

  `register-inventory.ts`

  ```ts
  import { createClient } from '@kindgi/sdk/client';
  import type { TenantId } from '@kindgi/sdk/types';

  const kindgi = createClient(); // KINDGI_API_URL, KINDGI_API_TOKEN, or the running kindgi dev

  await kindgi.mcp.endpoints.register({
    endpointId: 'inventory',
    name: 'Inventory',
    transport: 'streamable-http',
    config: { transport: 'streamable-http', url: 'http://localhost:8791/mcp' },
    scope: { kind: 'tenant', tenantId: process.env.KINDGI_TENANT_ID as TenantId },
  });

  const page = await kindgi.mcp.endpoints.list();
  console.log(page.data.map((e) => `${e.endpointId} (${e.transport})`));
  ```

  ```text
  [ 'inventory (streamable-http)' ]
  ```

  `KINDGI_TENANT_ID` is the id of the tenant your runtime serves.

- **Python**

  `register_inventory.py`

  ```python
  from kindgi.client import Kindgi

  client = Kindgi()  # KINDGI_API_URL, KINDGI_API_TOKEN, or the running kindgi dev

  client.mcp.endpoints.register(
      {
          "endpointId": "inventory",
          "name": "Inventory",
          "transport": "streamable-http",
          "config": {"transport": "streamable-http", "url": "http://localhost:8791/mcp"},
          "scopeKind": "tenant",
      }
  )

  for endpoint in client.mcp.endpoints.list().data:
      print(endpoint.endpoint_id, endpoint.transport)
  ```

  ```text
  inventory streamable-http
  ```

- **Java**

  `src/main/java/com/acme/app/RegisterInventory.java`

  ```java
  package com.acme.app;

  import com.kindgi.client.Kindgi;
  import com.kindgi.client.models.MCPEndpoint;
  import com.kindgi.client.models.MCPTransport;
  import com.kindgi.client.models.RegisterMCPEndpointBody;
  import com.kindgi.client.models.ScopeKind;

  public final class RegisterInventory {
    public static void main(String[] args) {
      Kindgi kindgi = Kindgi.create(); // KINDGI_API_URL, KINDGI_API_TOKEN, or the running kindgi dev

      kindgi.mcp().endpoints().register(RegisterMCPEndpointBody.builder()
          .endpointId("inventory")
          .name("Inventory")
          .transport(MCPTransport.STREAMABLE_HTTP)
          .config(RegisterMCPEndpointBody.Config.StreamableHttp.builder()
              .url("http://localhost:8791/mcp")
              .build())
          .scopeKind(ScopeKind.TENANT)
          .build());

      for (MCPEndpoint endpoint : kindgi.mcp().endpoints().list().data()) {
        System.out.println(endpoint.endpointId() + " " + endpoint.transport());
      }
    }
  }
  ```

  ```text
  inventory streamable-http
  ```

- **Scala**

  `src/main/scala/acme/app/RegisterInventory.scala`

  ```scala
  package acme.app

  import com.kindgi.client.Kindgi
  import com.kindgi.client.models.{MCPTransport, RegisterMCPEndpointBody, ScopeKind}
  import scala.jdk.CollectionConverters._

  object RegisterInventory {
    def main(args: Array[String]): Unit = {
      val kindgi = Kindgi.create() // KINDGI_API_URL, KINDGI_API_TOKEN, or the running kindgi dev

      kindgi.mcp().endpoints().register(RegisterMCPEndpointBody.builder()
        .endpointId("inventory")
        .name("Inventory")
        .transport(MCPTransport.STREAMABLE_HTTP)
        .config(RegisterMCPEndpointBody.Config.StreamableHttp.builder()
          .url("http://localhost:8791/mcp")
          .build())
        .scopeKind(ScopeKind.TENANT)
        .build())

      for (endpoint <- kindgi.mcp().endpoints().list().data.asScala)
        println(s"${endpoint.endpointId} ${endpoint.transport}")
    }
  }
  ```

  ```text
  inventory streamable-http
  ```

Or register it with `curl`:

```sh
curl -X POST "$KINDGI_API_URL/v1/mcp/endpoints" \
  -H "authorization: Bearer $KINDGI_API_TOKEN" -H 'content-type: application/json' \
  -d '{"endpointId":"inventory","name":"Inventory","transport":"streamable-http",
       "config":{"transport":"streamable-http","url":"http://localhost:8791/mcp"},
       "scopeKind":"tenant"}'
```

```json
{"endpointId":"inventory"}
```

The runtime connects, lists the server's tools and registers them. The `kindgi dev` terminal says how it went:

```text
    [runtime] <time> INFO  [mcp] MCP endpoint "inventory" synced — 2 published, 0 reinstated, 0 unchanged, 0 retired
```

The tools are now your tenant's:

- **TypeScript**

  ```sh
  pnpm exec kindgi tools get inventory.stock_level
  ```

- **Python**

  ```sh
  uv run kindgi tools get inventory.stock_level
  ```

- **Java**

  ```sh
  ./kindgiw tools get inventory.stock_level
  ```

- **Scala**

  ```sh
  ./kindgiw tools get inventory.stock_level
  ```

```json excerpt
{
  "id": "inventory.stock_level",
  "description": "Returns how many units of a SKU are in stock.",
  "version": "1.0.0",
  "input": {
    "type": "object",
    "title": "stock_levelArguments",
    "required": [
      "sku"
    ],
    "properties": {
      "sku": {
        "type": "string",
        "title": "Sku"
      }
    }
  },
  "output": {
    "type": "object",
    "additionalProperties": true
  },
  "transport": "mcp",
  …
}
```

Kindgi registers MCP tools as version `1.0.0`. A tool's input schema is the one the server gives. When the server declares an `outputSchema`, the tool's answer is the server's structured content, checked against it. Otherwise the answer is the text, parsed as JSON when the text is JSON. Servers built with the TypeScript MCP SDK often declare none: their `echo` tool answers `"Echo: hi"`.

## Call its tools

An agent lists an MCP tool by id, with a version range. Ask your coding agent: "Let my agent check stock levels with the inventory server's tool." For reference, it adds the tool to the agent's list:

- **TypeScript**

  ```ts excerpt
  // in agents/order-desk/index.ts
  tools: [
    { id: 'my-pack.lookup-order' as ToolId, version: '^0.1.0' },
    { id: 'inventory.stock_level' as ToolId, version: '^1.0.0' },
  ],
  ```

- **Python**

  ```python excerpt
  # in agents/order_desk.py
  tools=[lookup_order, {"id": "inventory.stock_level", "version": "^1.0.0"}],
  ```

- **Java**

  ```java excerpt
  // in src/main/java/mypack/agents/OrderDesk.java
        .tool(LookupOrder.TOOL)
        .tool("inventory.stock_level", "^1.0.0")
  ```

- **Scala**

  ```scala excerpt
  // in src/main/scala/mypack/agents/OrderDesk.scala
      .tool(LookupOrder.tool)
      .tool("inventory.stock_level", "^1.0.0")
  ```

Start a turn with a model that calls tools. Here it's Llama 3.1 on Ollama, as [Models](https://docs.kindgi.com/v0.1.8/guides/models.md) sets up. `--agent` is the agent, and `--input` its input:

- **TypeScript**

  ```sh
  pnpm exec kindgi runs start --agent=my-pack.order-desk --input='{"userMessage":"How many units of SKU-2 are in stock?"}'
  ```

- **Python**

  ```sh
  uv run kindgi runs start --agent=my-pack.order-desk --input='{"userMessage":"How many units of SKU-2 are in stock?"}'
  ```

- **Java**

  ```sh
  ./kindgiw runs start --agent=my-pack.order-desk --input='{"userMessage":"How many units of SKU-2 are in stock?"}'
  ```

- **Scala**

  ```sh
  ./kindgiw runs start --agent=my-pack.order-desk --input='{"userMessage":"How many units of SKU-2 are in stock?"}'
  ```

```json excerpt
"appended": [
  { "role": "user", "content": "How many units of SKU-2 are in stock?", … },
  {
    "role": "agent",
    "content": {
      "text": "",
      "toolCalls": [{ "id": "call_v7vwbcgs", "name": "inventory.stock_level", "arguments": { "sku": "SKU-2" } }]
    },
    …
  },
  {
    "role": "tool",
    "content": { "sku": "SKU-2", "units": 0 },
    "toolCall": { "toolId": "inventory.stock_level", "invocationId": "call_v7vwbcgs" },
    …
  },
  {
    "role": "agent",
    "content": "There are 0 units of SKU-2 in stock.",
    …
  }
],
```

A flow step names it in `ref`, like any tool: `{ id: 'stock', kind: 'tool', ref: 'inventory.stock_level', inputMapping: { sku: { path: 'runInput.sku' } } }`.

## Send a credential

To send a token, ask your coding agent: "My inventory MCP server wants a bearer token." You store the token itself, as below. For reference, it adds `secretRef` to the registration. Kindgi resolves the secret in the tenant's secrets when it connects, and sends it as `Authorization: Bearer <value>`:

```json excerpt
"secretRef": { "envName": "local", "name": "INVENTORY_TOKEN" }
```

Under `kindgi dev`, `local` is the pack's env files. If the secret isn't there when the endpoint is registered, no tools are registered, and the `kindgi dev` terminal says why:

```text
    [runtime] 07:23:26.303 WARN  [mcp] MCP endpoint "warehouse" sync failed: mcp-auth-unresolved: MCP endpoint "warehouse": its secretRef local/INVENTORY_TOKEN did not resolve: No secret "INVENTORY_TOKEN" for env "local" in .env, .env.local, .kindgi/secrets.env at /pack endpointId=warehouse
```

Store the secret with `kindgi secrets set INVENTORY_TOKEN --env=local --scope=tenant`. Then unregister the endpoint and register it again, as [When the server changes](https://docs.kindgi.com/v0.1.8/guides/tools/mcp-servers.md#when-the-server-changes) shows.

### Sign in with a user name and password

For a server that takes HTTP Basic, use `auth` instead of `secretRef`. A WordPress site's MCP Adapter takes it with an Application Password, and so do WooCommerce's MCP tools. Ask your coding agent: "My MCP server signs in with a user name and password." For reference, it adds this to the registration:

```json excerpt
"auth": {
  "scheme": "basic",
  "username": "kindgi-agent",
  "secretRef": { "envName": "local", "name": "SHOP_APP_PASSWORD" }
}
```

Kindgi sends `Authorization: Basic <base64 of username:password>`, the password resolved from the tenant's secrets when it connects. Give the agent its own WordPress user, with a role that allows only what it should do, and create the Application Password on that user's profile.

A user name or password the server turns down with a 401 fails the tool call with `mcp-auth-refused`. The runtime's log names the endpoint:

```text
20:22:48.769 WARN  [mcp] MCP endpoint "shop-bad" sync failed: mcp-auth-refused: MCP endpoint "shop-bad" answered 401 to its credential endpointId=shop-bad
```

On WordPress's side: [Application Passwords](https://developer.wordpress.org/rest-api/using-the-rest-api/authentication/#basic-authentication-with-application-passwords), the [MCP Adapter](https://github.com/WordPress/mcp-adapter) and [WooCommerce's MCP tools](https://developer.woocommerce.com/docs/features/mcp/).

### Sign in with OAuth client credentials

For a server behind an OAuth 2 token endpoint, such as a Drupal site's MCP Server with Simple OAuth, ask your coding agent: "My MCP server signs in with OAuth client credentials." For reference, it adds this to the registration:

```json excerpt
"auth": {
  "scheme": "oauth2-client-credentials",
  "tokenUrl": "https://cms.example.com/oauth/token",
  "clientId": "kindgi",
  "secretRef": { "envName": "local", "name": "CMS_CLIENT_SECRET" },
  "scope": "mcp_server"
}
```

Kindgi asks `tokenUrl` for an access token, with `grant_type=client_credentials`. It sends the token as `Authorization: Bearer <token>`:

- **Refreshed before it expires:** a fifth of the token's lifetime early, at most a minute. A 300-second token is replaced after 240 seconds. A token answered without `expires_in` is used for 60 seconds.

- **The client's credentials:** an `Authorization: Basic` header to the token endpoint by default. `"clientAuth": "client_secret_post"` sends `client_id` and `client_secret` in the form instead. `audience` is sent when it's set.

- **Never stored:** tokens live in the runtime's memory only, and each runtime instance gets its own. The secret is resolved at each token request, so a rotated secret is used from the next one.

- **The token URL** is https, or http to `localhost`, `127.0.0.1` or `[::1]`. A token endpoint on the runtime's own machine is reached only with `KINDGI_TENANT_HOST_ACCESS=local`, the default in `kindgi dev`. A deployed runtime's default, `deployed`, refuses it with `host-access-denied`.

- **When it fails:**

  - a token request that's refused fails the tool call with `mcp-auth-refused`, as with `invalid_client` or a scope the client may not have;
  - one that can't be made fails it with `mcp-auth-failed`, as when the token endpoint is down or its answer is malformed.

  Neither message includes the secret or a token. The runtime's log says which, and why:

  ```text
  20:22:48.608 WARN  [mcp] MCP endpoint "cms-down" sync failed: mcp-auth-failed: MCP endpoint "cms-down": its token request to localhost:18099 failed: fetch failed (ECONNREFUSED) endpointId=cms-down
  20:22:49.143 WARN  [mcp] MCP endpoint "cms-bad" sync failed: mcp-auth-refused: MCP endpoint "cms-bad": its token endpoint localhost:18081 refused the client (HTTP 401: invalid_client) endpointId=cms-bad
  ```

- **A token the server stops taking early,** with a 401: Kindgi asks for a new one and sends that request once more.

On Drupal's side: the [MCP Server](https://www.drupal.org/project/mcp_server) module, and [Simple OAuth](https://www.drupal.org/project/simple_oauth) for the token endpoint: a consumer with the client credentials grant, and the user its tokens act as.

### Send a key in a header of the server's own

For a server that takes an API key in a header such as `X-Api-Key`, a key and a secret in two headers, or `Authorization` with a prefix other than `Bearer `, use `scheme: "header"`. Ask your coding agent, for example: "My MCP server wants an API key and a secret, each in a header of its own." For reference, it adds this to the registration:

```json
"auth": {
  "scheme": "header",
  "headers": [
    { "name": "X-Api-Key", "secretRef": { "envName": "local", "name": "INVENTORY_KEY" } },
    { "name": "X-Api-Secret", "prefix": "Secret ", "secretRef": { "envName": "local", "name": "INVENTORY_SECRET" } }
  ]
}
```

Kindgi sends each header as `<name>: <prefix><secret>` on every request, the secrets resolved from the tenant's secrets when it connects. Reading the endpoint back shows the references only:

```json
"auth":{"scheme":"header","headers":[{"name":"X-Api-Key","secretRef":{"name":"INVENTORY_KEY","envName":"local"}},{"name":"X-Api-Secret","prefix":"Secret ","secretRef":{"name":"INVENTORY_SECRET","envName":"local"}}]}
```

- **1 to 4 headers**, each name once (case doesn't matter). `prefix` is optional (`Token `, `ApiKey `, …). It's not a secret: it's stored and read back as given.

- **Not a header the transport sets:** `Host`, `Content-Type`, `Accept`, `Mcp-Session-Id`, `traceparent` and the like are refused (`invalid-auth`):

  ```json excerpt
  {"error":{"code":"invalid-mcp-endpoint","message":"endpoint \"inventory-reserved\" auth.headers[0].name \"Mcp-Session-Id\" is a header the MCP transport sets itself","details":{"reason":"invalid-auth"},…}}
  ```

- **A key the server turns down** (it answers 401) fails the tool call with `mcp-auth-refused`, as a wrong Basic password does.

- **A secret a header can't carry**, such as a value pasted with a line break inside it, fails with `mcp-auth-unresolved`. The log names the header, never the value:

  ```text
  23:53:08.218 WARN  [mcp] MCP endpoint "inventory-pasted" sync failed: mcp-auth-unresolved: MCP endpoint "inventory-pasted": its credential for the header "X-Api-Key" has a line break, which a header can't carry: store the secret without it endpointId=inventory-pasted
  ```

### A secret that isn't set

With any scheme, a secret that isn't set fails the tool call with `mcp-auth-unresolved`. The log names the secret and where it looked, never a value:

```text excerpt
21:02:11.089 WARN  [mcp] MCP endpoint "cms-nosecret" sync failed: mcp-auth-unresolved: MCP endpoint "cms-nosecret": its auth client secret local/CMS_SECRET_NEVER_SET did not resolve: No secret "CMS_SECRET_NEVER_SET" for env "local" in .env, .env.local, .kindgi/secrets.env at … endpointId=cms-nosecret
```

### Signed in, but no tools

A sync that signed in says what it found:

```text
21:00:51.837 INFO  [mcp] MCP endpoint "shop" synced — 3 published, 0 reinstated, 0 unchanged, 0 retired endpointId=shop published=3 reinstated=0 unchanged=0 retired=0
21:00:52.218 INFO  [mcp] MCP endpoint "cms" synced — 1 published, 0 reinstated, 0 unchanged, 0 retired endpointId=cms published=1 reinstated=0 unchanged=0 retired=0
```

One whose counts are all 0 signed in too: `synced — 0 published, 0 reinstated, 0 unchanged, 0 retired`. The server listed no tools to that account. Which tools a server lists, and to whom, is set on the server's side. Check that the account the credentials stand for may use them.

### What `auth` can't be set with

`secretRef` stays the plain bearer form, and `auth` is the other schemes. A registration with both is refused (`invalid-mcp-endpoint`, reason `auth-with-secret-ref`), and so is `auth` on a `stdio` endpoint (`auth-on-stdio`: a stdio server gets no headers). A header `auth` sends can't be in `config.headers` too:

- `Authorization`, beside `basic` or `oauth2-client-credentials` (`auth-with-authorization-header`);

- a name in `auth.headers` (`auth-header-in-config`):

  ```json excerpt
  {"error":{"code":"invalid-mcp-endpoint","message":"endpoint \"inventory-twice\" sets the header \"x-api-key\" in both auth.headers and config.headers: auth sends it. Drop it from config.headers.","details":{"reason":"auth-header-in-config"},…}}
  ```

### Not in `config.headers`

`config.headers` is for plain headers: they're stored, and read back, as given. A header whose name says it carries a credential is refused (`invalid-mcp-endpoint`, reason `credential-in-headers`). The refusal names the header, never its value, and shows the `auth` that sends it by reference:

```json excerpt
{"error":{"code":"invalid-mcp-endpoint","message":"endpoint \"inventory-plain\" puts a credential in config.headers (\"X-Api-Key\"), where it's stored and shown as given. Store each value as a secret and send it by reference: auth: { scheme: \"header\", headers: [{ name: \"X-Api-Key\", secretRef: { envName, name } }] }.","details":{"reason":"credential-in-headers"},…}}
```

Which names count:

- `Authorization`, `Proxy-Authorization` and `Cookie`;
- a name with a part, between `-` or `_`, of `token`, `secret`, `password`, `passwd`, `apikey`, `credential`, `credentials`, `signature`, `session`, `auth`, `authorization` or `authentication` (`X-Auth-Token`, `X-Session-Id`, `X-Client-Secret`, `X-Authorization`);
- a name with the parts `api-key`, `access-key`, `private-key`, `auth-key`, `subscription-key`, `functions-key`, `application-key`, `app-key`, `master-key`, `client-key` or `partner-key` (`X-Api-Key`, `Ocp-Apim-Subscription-Key`, `x-functions-key`, `DD-APPLICATION-KEY`).

`X-Tenant`, `X-Request-Id`, `X-Region`, `X-Key-Id`, `X-Idempotency-Key` or `X-Cache-Key` are plain headers: a lone `key` doesn't count. A name that's refused but isn't a secret costs only this: store its value as a secret too.

For `Authorization: Bearer <token>`, use `secretRef`. For any other header, use `auth` with `scheme: "header"`.

#### An endpoint registered with one by an earlier runtime

It keeps working, and:

- **reads back `[redacted]`** for each such header, its name kept:

  ```json
  "config":{"url":"http://127.0.0.1:18766/mcp","headers":{"X-Region":"eu","X-Api-Key":"[redacted]","X-Api-Secret":"[redacted]"},"transport":"streamable-http"}
  ```

- **never follows a redirect**, as an endpoint with `auth` or `secretRef` doesn't. A server that answers 3xx fails the call with `mcp-auth-failed`:

  ```text
  23:53:20.206 WARN  [mcp] MCP endpoint "inventory-moved" skipped: mcp-auth-failed: MCP endpoint "inventory-moved" redirected (HTTP 307) a request carrying a credential header in its config.headers (X-Api-Key, X-Api-Secret): register the URL it redirects to endpointId=inventory-moved transport=streamable-http
  ```

- **is named, never its value:** by the runtime at every start (`WARN` `mcp-header-credential`), by its [`check mcp-header-credentials`](https://docs.kindgi.com/v0.1.8/deploy/operate.md#checks-you-can-run-before-upgrading), and by `kindgi doctor` against the runtime it points at:

  ```text
    ! MCP endpoint headers: 2 MCP endpoints keep a credential in plaintext config.headers, from before that was refused. They still work, but each value is stored as given, not as a secret.
        ✗ inventory-legacy: X-Api-Key, X-Api-Secret
        ✗ inventory-moved: X-Api-Key, X-Api-Secret
        Fix: Store each value as a secret (kindgi secrets set <NAME> --env=<name> --scope=tenant), then unregister the endpoint and register it again with the header in auth: { scheme: "header", headers: [{ name: "<header>", secretRef: { envName, name } }] }.
  ```

To move one: store each header's value as a secret (`kindgi secrets set INVENTORY_KEY --env=local --scope=tenant`), [unregister the endpoint](https://docs.kindgi.com/v0.1.8/guides/tools/mcp-servers.md#when-the-server-changes), and register it again with `auth` in place of the header.

## Send the run's trace

Ask your coding agent: "I want to match my MCP server's logs to the runs that called it." For reference, it adds `"sendTraceparent": true` to the registration, and each tool call to the server carries the W3C `traceparent` of the run that made it, as a request header. The server's logs can then be matched to the run, as [Follow a request or a run](https://docs.kindgi.com/v0.1.8/deploy/logs.md#follow-a-request-or-a-run) shows. Only the ids go: the trace id and a span id, never the tool's input or output. It's off by default, and HTTP transports only. A `stdio` server gets no headers, so `true` there is refused with `invalid-mcp-endpoint`, reason `invalid-send-traceparent`.

## When the server changes

The runtime lists an endpoint's tools when the endpoint is registered. It doesn't watch the server. To pick up new or changed tools, unregister the endpoint and register it again. A runtime serving one tenant, the default, also lists every endpoint's tools each time it starts, so restarting it works too. A runtime [serving many tenants](https://docs.kindgi.com/v0.1.8/deploy/serve-many-tenants.md) (`KINDGI_TENANCY=multi`) lists none when it starts: there, only unregistering and registering again does. `inventory` in the path is the endpoint's id:

```sh
curl -X POST "$KINDGI_API_URL/v1/mcp/endpoints/inventory/unregister" \
  -H "authorization: Bearer $KINDGI_API_TOKEN"
```

```json
{"endpointId":"inventory","unregistered":true}
```

Unregistering retires the endpoint's tools. Registering it again brings them back.

If the server is down when a tool is called, the call fails and the next one reconnects:

```json excerpt
  "failureMessage": "handler-error: Tool \"inventory.stock_level\" handler threw: mcp-call-failed: MCP endpoint \"inventory\" tool \"stock_level\": tools/call \"stock_level\" failed: fetch failed (the next call reconnects)",
```

## Transports

- **`streamable-http`**: `{ transport: 'streamable-http', url, headers? }`. Use it wherever you can. `headers` are sent, and read back, as given, so they're for plain headers only: a credential goes in `auth` or `secretRef` ([Not in `config.headers`](https://docs.kindgi.com/v0.1.8/guides/tools/mcp-servers.md#not-in-configheaders)).

- **`http-sse`**: the older HTTP and Server-Sent Events transport, `{ transport: 'http-sse', url, sseUrl?, headers? }`, with `url` the server's SSE endpoint (`http://localhost:8793/sse` for the Python SDK).

- **`stdio`**: `{ transport: 'stdio', command, args?, env? }`. The runtime runs the command in its own container, as its own user, under `kindgi dev` too. A deployment refuses it, because `KINDGI_TENANT_HOST_ACCESS` is `deployed` outside development:

  ```json excerpt
  {"error":{"code":"host-access-denied","message":"MCP endpoint \"files\" uses the stdio transport, which runs a command on the server's host; KINDGI_TENANT_HOST_ACCESS=deployed refuses that. Run the MCP server over HTTP (streamable-http) instead.", …}}
  ```

  See [Security](https://docs.kindgi.com/v0.1.8/concepts/security.md#what-a-tenant-cant-make-the-server-do).

## Good to know

- An MCP tool always counts as one that changes things: it doesn't run in a [dry run](https://docs.kindgi.com/v0.1.8/guides/tools/read-only-tools.md#dry-runs), and an approval gate asks before it.
- A tool's schemas are read in the JSON Schema dialect their `"$schema"` declares: draft-06, draft-07, 2019-09 or 2020-12. Servers built with the TypeScript MCP SDK declare draft-07. 2020-12 is the default when they declare none. Kindgi skips a tool whose schema declares another dialect or doesn't compile, and the runtime's log names the tool.
- An endpoint is registered at a scope: `tenant`, as here, or a project, with `"scopeKind": "project", "scopeId": "<project id>"`. The [API reference](https://docs.kindgi.com/v0.1.8/reference/api/operations/tags/mcp.md) has the registration's fields.

## Next

- [Mark a tool read-only](https://docs.kindgi.com/v0.1.8/guides/tools/read-only-tools.md)
- [Give a tool a secret](https://docs.kindgi.com/v0.1.8/guides/tools/give-a-tool-a-secret.md)
- [Call an HTTP API without code](https://docs.kindgi.com/v0.1.8/guides/tools/http-tools.md)
