If your team needs to connect a tool to a Tedix assistant, this guide explains how applications provide that access while keeping each organization’s data and permissions separate.
First, connect your assistant using Use Tedix in ChatGPT, Codex and Claude. This page explains how to connect tools and applications to Tedix.
Tenant-scoped connections
A shared application definition can be connected separately for each organization. Credentials and policy remain tenant-scoped, so installing the same integration for two customers does not mix their data or authority.
Configured at runtime
Tedix applications use saved settings that persist over time. Tool descriptions, required permissions, identity rules, and interface components can change without releasing separate software for every organization.
MCP and durable work
MCP calls can participate in durable Tedix work:
- tools are attributed to the worker and run that used them;
- long-running operations can return durable task identifiers;
- policies can require confirmation before sensitive mutations;
- results can be attached to rationale and audit records.
MCP provides the connection. Tedix adds organization-specific identity, access controls, long-running work, and audit records.
Trace one tool
One external-transport test starts with this configuration:
{
"transport": "external",
"method": "GET",
"baseUrl": "https://api.example.test",
"endpoint": ""
}Run it from a source checkout:
bun run --cwd apps/mcp test:run src/mcp/handler-external.test.ts -t 'allows OpenAPI root path'The test mocks the upstream response and verifies { "ok": true }. It needs
no provider account and does not install a live integration. Follow
the test (apps/mcp/src/mcp/handler-external.test.ts) into ToolHandler
(apps/mcp/src/mcp/handler.ts) to see execution, then the tool schema
(packages/db/src/schema/tools.ts) for persisted configuration. Live tools also require tenant-owned connection
credentials and an explicit scope decision; see the
MCP agent guide in the source tree (apps/mcp/AGENTS.md).
Optional Interaction reply events
An MCP app can opt into work.interaction.responded using
mcpConfig.interactionEvents: true. This adds MCP 2.0 event discovery,
polling and webhook subscriptions on the authenticated MCP endpoint. Polling
uses a messaging read scope (mcp:messaging.read) and optional request_id,
work_item_id and responder_type filters. Each webhook subscription
requires an exact organization and Interaction request ID, human OAuth access,
and a verified public HTTPS callback. Notifications contain saved record IDs;
the host retrieves the answer through the existing read tool.
Delivery uses signed callbacks, duplicate suppression and bounded retries. Access is rechecked before delivery. Subscriptions expire no later than the credential; the host must refresh them. Webhook notifications are not replayed, and a saved answer is not guaranteed to produce one. If you miss a notification, read the request; a successful callback acknowledges receipt, not task completion.
OpenAI currently supports these events in ChatGPT Work on web, desktop Work with Cloud selected, and dots. Local Codex desktop wake-up is not proved by this integration. Local context hooks remain optional and require host trust; native Codex Goals own continuation of executable work. See OpenAI MCP Events.
Poll Work and skill-run events
The same flag also adds ledger events to events/list:
work.item.state_changed (accepted, completed, cancelled, Attempt settled or
expired, registered lane stale),
work.approval.requested, work.item.commented,
work.interaction.requested and skill.run.failed. Each poll needs human OAuth
that selects organization_id, plus a Work read scope (mcp:work.*) or, for
skill runs, a skills scope. Reads come from the organization’s own ledgers, so
an agent never sees another organization’s events. Payloads carry IDs and
enums only. Comment events omit bodies and metadata; Interaction request
events omit subjects, prompts and metadata. Filter comments by work_item_id
and author_type, or requests by work_item_id, target_id and kind.
Reply events (work.interaction.responded) can also be polled with a messaging
read scope (mcp:messaging.read), filtered by request_id, work_item_id and
responder_type (user, tedi or external_agent). Reply payloads contain
only organization, request and response IDs. Re-read the record before acting.
home.run.finished reports completed, failed or needs_input (an approval
pause or unanswered Home question/handoff) for a Home run or delegated child. It requires a messaging read scope.
Every delivery checks the canonical conversation grants: private chats reach
their owner and shared readers, never other members merely because they belong
to the organization. Child events use the parent Home conversation’s grants,
including multi-tedi plan branches. Payloads contain organization, conversation,
run and optional parent-run IDs plus status; titles and answers stay out of the
feed. Filter with conversation_id to follow one chat.
Claude and Codex channels deliver these events as Home finished: <title> with
the run ID. They read the title through the existing authorized Home list.
Use tedix run <id> to read the result; a delegated child ID resolves to its
Home parent and child evidence. Run a channel before starting the turn; its
initial cursor starts from now. Use --param conversation_id=<id> to route
only that Home chat to its originating host session.
curl -s https://<app-host>/mcp -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" -H "MCP-Protocol-Version: 2026-07-28" \
-H "Mcp-Method: events/poll" -d '{"jsonrpc":"2.0","id":1,"method":"events/poll",
"params":{"name":"work.item.state_changed","cursor":null,
"arguments":{"organization_id":"00000000-0000-4000-8000-000000000001"}}}'A null cursor starts from now and returns a fresh cursor. Store it and pass
it on the next poll. Poll again at once while hasMore is true, otherwise
after nextPollMs. Neither Claude Code nor the Codex CLI polls MCP events
for a custom server today; tedix channel (below) polls for either. Without
the webhook opt-in below, events/subscribe answers -32026 for these
five events; reply webhooks still require an exact request. Discovery lists the
extension as io.modelcontextprotocol/events under capabilities.extensions.
Deliver events into a Claude Code or Codex session
Claude Code starts a turn on a channel message from a local stdio MCP server,
not on an event from a remote server. tedix channel claude is that server: it
polls these events with your stored login and sends each one to the session
as a short ID-only notice. Register it in .mcp.json:
{
"mcpServers": {
"tedix-events": {
"command": "tedix",
"args": ["-w", "connect", "--organization", "<slug>", "channel", "claude"]
}
}
}Start an interactive session with
claude --dangerously-load-development-channels server:tedix-events; channels
are a Claude Code research preview and do not load with claude -p. Name
events after claude to limit them, and add --param work_item_id=<uuid> (or
another inputSchema key) to filter.
The Codex CLI has no inbound path for a local thread.
tedix channel codex <thread-id> runs until interrupted and sends each batch
of events to that thread through codex queue; when no Codex window takes it,
the thread resumes headless with codex exec resume in the current directory.
The same filters apply. ChatGPT Work receives these events by webhook instead
(below).
Subscribe to Work and skill-run events by webhook
Set mcpConfig.eventWebhooks: true next to interactionEvents to let hosts
subscribe to the same ledger events instead of polling. events/list then
shows "delivery": ["poll", "webhook"] for them. A subscription needs the
same human OAuth grant and read scope as a poll, a public HTTPS callback that
echoes the verification challenge, and a whsec_ secret of 24 to 64 bytes:
curl -s https://<app-host>/mcp -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" -H "MCP-Protocol-Version: 2026-07-28" \
-H "Mcp-Method: events/subscribe" -d '{"jsonrpc":"2.0","id":1,
"method":"events/subscribe","params":{"name":"work.item.state_changed",
"arguments":{"organization_id":"00000000-0000-4000-8000-000000000001"},
"delivery":{"mode":"webhook","url":"https://hooks.example.com/tedix",
"secret":"whsec_<base64 of 32 random bytes>"},"ttlMs":3600000}}'The result carries id, cursor and refreshBefore. The lease lasts at most
24 hours and never outlives the bearer. Send the same request with a fresh
token before refreshBefore to renew it; renewal keeps the delivered
position. Pass a cursor from events/poll to resume from it. Send
events/unsubscribe with the same name, arguments and delivery.url to
stop.
Tedix checks for new events every two minutes. Before each pass it rechecks
the token, consent, selected organization, read scope and the app opt-in;
access that has ended stops delivery. Each event is a POST signed like the
Interaction reply event (webhook-id, webhook-timestamp,
webhook-signature: v1,<base64 HMAC-SHA256 of id.timestamp.body>). The body is
{eventId, name, timestamp, data, cursor}.
Delivery is at least once, so deduplicate on webhook-id. A failed page is
retried with backoff from two to 30 minutes. After six failures in a row, or a
410, the subscription is marked unhealthy and delivery stops. Events from
that period are dropped; subscribe again to restart from now. Each
subscription receives at most 300 events per hour, and later events wait for
the next hour. A caller can hold 20 subscriptions per organization.
Configured native catalog tools
An app can configure tools with transport: "catalog" and the closed
catalog/search or catalog/describe endpoint. Search accepts query, namespace,
bounded paging, and schema inclusion options; describe accepts an exact
namespace.tool callable. These operations use the same request-local tool discovery as Code Mode
without creating a separate isolated tool runner. Discovery reports whether
a tool is authorized to run; it does not grant permission.
In Code Mode, discover.describe("namespace.tool") returns the full input
schema, description and annotations. Output schemas are omitted by default to
keep the result small. Use
discover.describe({ callable: "namespace.tool", includeOutputSchema: true })
when you need the output schema. The native catalog/describe transport retains
its full descriptor for existing clients. Native CLI Work commands explicitly
request output schemas through catalog search and validate results against them.
Each enabled row needs its own explicit capability mapping. Missing, empty,
invalid, namespace-only or wildcard-only mappings do not expose the tool.
The compact gateway lists the base tools plus its actually configured,
caller-permitted catalog rows using the stored tool metadata and stable paging.
No configured rows means the base surface stays unchanged. Aggregate-only rows
are available after normal session construction, not through compact list
hydration. The stateless lane still omits ask.
Catalog operations execute synchronously in the original caller context; async-task replay and executable code inputs are refused. Arbitrary Code Mode programs still run in a fresh, anonymous, isolated environment with no ambient network access. Native catalog calls can still use the existing discovery API and ranking services.
Configured catalog rows use the shared strict search or describe input declaration.
They require capability-map mode (enforcePolicies: false); policy-mode rows do not expose this transport.