kortecxdocs
Tools

Connections (MCP)

Plug an outside service into the agentic runtime — name where it lives, and the runtime dials it, asks what it can do, and writes the answer down.

A connection is an outside service the agentic runtime is allowed to act on: read a Slack channel, search a Notion workspace, draft a Gmail message, post to Discord.

You do not write code to add one. You name where the service lives. The runtime then goes and knocks on the door — it opens a session, asks the service what tools do you have?, and writes the answer into its own tool registry. From that moment those tools exist for your agents by name.

The protocol it knocks with is MCP (the Model Context Protocol) — a small, standard way for a program to publish a list of callable tools. Anything that speaks MCP can be connected, not only the four services that ship in the box.

Connections dial. `kx tools register` does not.

kx tools register records that a tool exists. It does not go out and call the host — dialing a registered host is a Cloud capability. kx connections is the OSS path that really calls out. If you want a working integration today, use this page.

Before you start

The connection commands talk to a running runtime, so start one:

kx serve --dev-allow-local

Leave that running in its own terminal. Everything below runs in a second one.

The four bundled connectors

Four connectors are maintained alongside kortecx as small standalone programs. Each is a separate process — it is never linked into the runtime — and each reads its credential from a name, never from a value you type into a command that uses it.

Provider--providerTool idsCredential name
Slackslackslack/post_message · slack/read_channel · slack/search · slack/list_channelsKX_SLACK_CREDENTIAL
Notionnotionnotion/search · notion/read_page · notion/create_page · notion/append_blockKX_NOTION_CREDENTIAL
Gmailgmailgmail/search · gmail/read · gmail/draft · gmail/sendKX_GMAIL_CREDENTIAL
Discorddiscorddiscord/send_message · discord/read_channel · discord/list_channelsKX_DISCORD_CREDENTIAL

A connector is its own binary, so it has to be present on this machine before you connect anything — beside your kx executable, or on your PATH. kx connections doctor is a local check: it does not need the runtime and does not touch the network.

kx connections doctor
kx connections doctor --provider slack --json

For each provider it reports whether the connector binary was found beside your kx executable, found on your PATH, or not found at all — and, when it is missing, the exact cargo install line that builds it from a checkout.

Connect Slack in two commands

Register the dial

kx connections add --provider slack

--provider slack fills in three things from a built-in catalogue: the connection name (slack), the program to run (kx-connector-slack), and the credential name the connector will read (KX_SLACK_CREDENTIAL).

This command is not a bookkeeping entry. The runtime spawns the connector, completes the MCP handshake, asks for its tool list, and registers each tool it gets back. The output tells you how many tools were discovered.

Set the credential by name

kx secrets set --name KX_SLACK_CREDENTIAL --value '{"bot_token":"xoxb-…"}'

The value is write-only. kx secrets set sends it once into the local secret store and no command ever reads it back — kx secrets list prints stored names only (add --json and you also get created/updated timestamps, never the value). The connection you registered above holds the name, never the token, so nothing sensitive sits in the connections database, in a command you re-run, or in an exported app.

You can set the secret before or after connections add. Listing tools does not need it; firing one does.

Prove it works

kx connections fire --name slack --tool list_channels --args '{}'

fire is the operator diagnostic — it calls exactly one tool, live, through the same path an agent would use. It answers the only question that matters at this stage: does this connector really work with this token?

The other three connect the same way:

kx connections add --provider notion
kx secrets set --name KX_NOTION_CREDENTIAL --value '{"token":"secret_…"}'

kx connections add --provider gmail
kx secrets set --name KX_GMAIL_CREDENTIAL --value '{"client_id":"…","client_secret":"…","refresh_token":"…"}'

kx connections add --provider discord
kx secrets set --name KX_DISCORD_CREDENTIAL --value '{"bot_token":"…"}'

You hold your own token

In OSS you bring the credential yourself: a Slack bot token, a Notion integration token, Gmail client credentials, a Discord bot token. There is no "sign in with…" button. OAuth and device-flow login, and a hosted credential marketplace, are Cloud capabilities — the OSS console shows them as a disabled affordance rather than pretending. Connections and secrets are also per-machine and operator-global here; per-user credential isolation on one shared instance is Cloud.

Slack has a search. Notion has a search. Gmail has a search. They cannot collide, because the runtime namespaces every discovered tool by the connection name you registered it under:

<connection name>/<remote tool name>

So the three land as slack/search, notion/search and gmail/search. The connection name is yours to choose (--name), and it is the half of the id an agent grant is written against. If you register Slack as --name work-slack, the tool is work-slack/search — and an agent must be granted work-slack/search, not slack/search.

Because the namespace is exact, removing a connection is exact too: kx connections remove deregisters everything under that prefix and nothing else.

A name with a slash in it is refused

The connection name namespaces tool ids, so it may not contain /. The runtime rejects the registration rather than producing an ambiguous id.

What "dialing" actually does

When you run add (or discover, or restart the runtime), the sequence is the same one every time:

Vet the destination

For an HTTP connection the host is checked before anything is opened. A host written as an IP address that is loopback, private, link-local, carrier-NAT, multicast or a metadata address is refused outright; a DNS name is admitted and then re-checked against the address it actually resolves to at dial time. A credential embedded in the URL (user:pass@host) is refused — credentials go through the by-name path only. If you set a host allowlist (KX_SERVE_TOOL_HOST_ALLOWLIST on the runtime), it is re-checked on every dial, so tightening it later is retroactive rather than grandfathered.

Open a session and initialize

The MCP handshake. The runtime records the protocol version the server negotiated, for diagnostics — an older or newer server still dials.

Ask tools/list

The service returns its tools with their input schemas. This is the "asks what it can do" step, and it is what makes the connection real rather than declared.

Write the answer down

Each tool is registered into the durable tool registry under <connection>/<tool>, and a firing capability is registered on the broker. The registry survives restarts, and on restart the runtime re-dials every stored connection in the background so the tools come back.

Reachability has exactly one definition — the full handshake plus tools/list. add, test and discover all run that same probe, so they can never disagree with each other about whether a server is up.

A failed dial is recorded honestly. The connection is stored with health unreachable and 0 tools rather than being silently dropped or reported as a success. Fix the service, then run discover again.

Day-to-day commands

kx connections list                                            # every connection + health + tool count
kx connections test --name slack                               # is it reachable right now?
kx connections discover --name slack                           # re-dial and re-read its tool list
kx connections fire --name slack --tool search --args '{"query":"budget"}'
kx connections remove --name slack                             # drop it and its slack/* tools

list shows each connection's health: connected, unreachable, or unknown before the first dial. Add --json to any of these for a machine-readable form.

Run discover after the service gains or loses a tool. test when you suspect the token expired or the service is down.

Connecting something that is not bundled

Any MCP server works. Give it a name yourself and point at it — a local program over stdio, or a remote endpoint over HTTP:

# a local MCP server you built, spoken to over stdio
kx connections add --name my-tools --command /usr/local/bin/my-mcp-server --arg --stdio

# a remote MCP server over HTTP, refusing plaintext, credential by name
kx connections add --name github \
  --url https://mcp.github.example/rpc \
  --tls-required \
  --credential-ref GH_MCP_TOKEN

The transport is inferred: --command means stdio, --url means HTTP. For HTTP, the credential you store under that name is used as the full Authorization header value, so store it complete (for example Bearer …).

By default each tool call opens its own short session. Pass --stateful (or --session-mode stateful) if the server needs one session reused across calls.

To build your own connector rather than dial someone else's, see Authoring a connector.

Letting an agent use the connection

Registering a connection makes its tools exist. It does not make them fire. A tool fires only when a run's grant names it — presence is never permission.

In an app, declare which connector the app dials, and separately list which tools the agent may fire:

# check the wiring first — this is the operator diagnostic, not an agent run
kx connections fire --name slack --tool read_channel --args '{"channel":"C0123ABCD"}'

Then author the app with one of the SDKs, and run it with kx app run <handle> --wait.

import kortecx as kx

app = (
    kx.app("launch-digest")
      .blueprint(
          kx.flow().agent(
              "Summarise the last 20 messages in #launch and post a one-line digest.",
              tools=["slack/read_channel", "slack/post_message"],
          )
      )
      .with_slack()                       # declares the bundled Slack connector
      .secrets(["KX_SLACK_CREDENTIAL"])   # scopes the run to that credential name
)
app.save(handle="apps/local/launch-digest")
import { app, flow } from "@kortecx/sdk";

const launchDigest = app("launch-digest")
  .blueprint(
    flow().agent(
      "Summarise the last 20 messages in #launch and post a one-line digest.",
      { tools: ["slack/read_channel", "slack/post_message"] },
    ),
  )
  .withSlack()
  .secrets(["KX_SLACK_CREDENTIAL"]);

.with_slack() / .withSlack() says this app dials the bundled Slack connector. The agent's tools list says these are the ones it may fire. Grant them by the connection name you registered — slack/read_channel — not by the connector program name.

An agent step needs a model being served by the runtime; kx connections fire does not. See Serving.

Because the app carries only names, an app you hand to a colleague resolves against their connections and their credentials. It carries no authority of its own.

The same idea covers multiple agents: agents chain inside one app, as steps of its blueprint (or with kx swarm --pattern supervisor). One app does not call another app.

Approvals are opt-in — and they cover every MCP tool

Every tool discovered over MCP is registered as world-mutating, whether it reads or writes. That classification only bites when the run asks for a human gate: start the run with kx app run <handle> --require-approval, or set KX_SERVE_REQUIRE_APPROVAL=1 on the runtime so new runs default to it. With the gate on, each MCP tool call is held for your decision before it fires — including a read like slack/read_channel. With it off, the calls fire inside the run. See Approvals.

When it does not work

`unreachable` right after `kx connections add --provider …`

The connector program is not installed beside kx or on your PATH. Run kx connections doctor — it names the exact binary it looked for and where.

A tool call that fails with an authorization error usually means the credential name is set but the value is wrong or expired. Re-run kx secrets set --name … --value … and then kx connections fire … to confirm.

A connection that dialed once and now reports unreachable means the service or the token changed underneath it. kx connections test --name <name> re-checks; kx connections discover --name <name> re-reads the tool list.

If dials are being refused faster than you expect, note that each connection has its own budget: by default a burst of 20 dials per connection, refilling at 10 per second. Repeated add/test/discover in a tight loop will be throttled per server.

Where to go next