Back to Copilotkit

Tools and context

showcase/shell-docs/src/content/docs/channels/tools.mdx

1.64.27.2 KB
Original Source

Channel tools let your agent call application code while it works in Slack or Teams. Context gives the agent information it should consider without exposing another callable action.

Install the schema library used in these examples:

bash
npm install zod

Add a channel-level tool

Use defineChannelTool for actions that are available in every conversation. The parameter schema becomes the tool's JSON schema and types the handler.

ts
import { defineChannelTool } from "@copilotkit/channels";
import { z } from "zod";

export const getIncident = defineChannelTool({
  name: "get_incident",
  description: "Read the current status of an incident by its id.",
  parameters: z.object({
    incidentId: z.string().describe("Incident id, for example INC-421"),
  }),
  async handler({ incidentId }) {
    const incident = await incidents.get(incidentId);
    return incident; // objects are serialized for the model
  },
});

Register it when you create the Channel:

<FrontendOnly frontend="slack">
ts
import { createChannel } from "@copilotkit/channels";
import { makeAgent } from "./agent.js";
import { getIncident } from "./tools.js";

function required(name: string): string {
  const value = process.env[name];
  if (!value) throw new Error(`Missing ${name}`);
  return value;
}

const channel = createChannel({
  name: required("CHANNEL_CODE"),
  provider: "slack",
  agent: makeAgent,
  tools: [getIncident],
});
</FrontendOnly> <FrontendOnly frontend="teams">
ts
import { createChannel } from "@copilotkit/channels";
import { makeAgent } from "./agent.js";
import { getIncident } from "./tools.js";

function required(name: string): string {
  const value = process.env[name];
  if (!value) throw new Error(`Missing ${name}`);
  return value;
}

const channel = createChannel({
  name: required("CHANNEL_CODE"),
  provider: "teams",
  agent: makeAgent,
  tools: [getIncident],
});
</FrontendOnly>

Return information the model can act on:

  • Return raw objects or arrays for data tools.
  • Return a short confirmation when the handler already posted UI.
  • Return or throw the real failure reason so the model can recover.
  • Do not manually JSON.stringify successful data.

Add stable context

Channel context is sent on every agent run:

<FrontendOnly frontend="slack">
ts
import { createChannel } from "@copilotkit/channels";
import { makeAgent } from "./agent.js";

function required(name: string): string {
  const value = process.env[name];
  if (!value) throw new Error(`Missing ${name}`);
  return value;
}

const channel = createChannel({
  name: required("CHANNEL_CODE"),
  provider: "slack",
  agent: makeAgent,
  context: [
    {
      description: "Support policy",
      value:
        "Never disclose credentials. Escalate Sev-1 incidents immediately.",
    },
    {
      description: "Response style",
      value: "Use short paragraphs and put the next action first.",
    },
  ],
});
</FrontendOnly> <FrontendOnly frontend="teams">
ts
import { createChannel } from "@copilotkit/channels";
import { makeAgent } from "./agent.js";

function required(name: string): string {
  const value = process.env[name];
  if (!value) throw new Error(`Missing ${name}`);
  return value;
}

const channel = createChannel({
  name: required("CHANNEL_CODE"),
  provider: "teams",
  agent: makeAgent,
  context: [
    {
      description: "Support policy",
      value:
        "Never disclose credentials. Escalate Sev-1 incidents immediately.",
    },
    {
      description: "Response style",
      value: "Use short paragraphs and put the next action first.",
    },
  ],
});
</FrontendOnly>

Keep this list stable and small. Fetch frequently changing data through a tool instead of embedding stale snapshots in every prompt.

Use provider and user context for one turn

On a managed Channel, the native origin is on message.platform ("slack" or "teams"). thread.platform and a channel-level tool's ctx.platform identify the managed adapter as "intelligence", so do not use them to branch on the native provider.

Add native context when you run the agent:

ts
channel.onMessage(async ({ thread, message }) => {
  await thread.runAgent({
    prompt: message.contentParts?.length
      ? [
          ...(message.text
            ? [{ type: "text" as const, text: message.text }]
            : []),
          ...message.contentParts,
        ]
      : message.text,
    context: [
      { description: "Originating platform", value: message.platform },
      {
        description: "Requesting user",
        value: message.user.name ?? message.user.id,
      },
    ],
  });
});

If a tool must be scoped to the current person, define it inside the handler and pass it for that run. Its closure captures the trusted inbound identity:

ts
channel.onMessage(async ({ thread, message }) => {
  const listMyApprovals = defineChannelTool({
    name: "list_my_approvals",
    description: "List approvals assigned to the requesting user.",
    parameters: z.object({}),
    async handler() {
      return approvals.forPlatformUser(message.platform, message.user.id);
    },
  });

  await thread.runAgent({
    prompt: message.text,
    tools: [listMyApprovals],
  });
});

Treat platform user ids as external identifiers. Resolve them to an authenticated application identity before allowing a consequential write.

Render a tool result as native UI

A tool can post a portable JSX message and return a short acknowledgement to the model:

tsx
import { defineChannelTool } from "@copilotkit/channels";
import { Header, Message, Section } from "@copilotkit/channels/ui";
import { z } from "zod";

export const showIncident = defineChannelTool({
  name: "show_incident",
  description: "Display an incident summary in the conversation.",
  parameters: z.object({
    id: z.string(),
    status: z.string(),
    summary: z.string(),
  }),
  async handler({ id, status, summary }, { thread }) {
    await thread.post(
      <Message>
        <Header>{`${id} · ${status}`}</Header>
        <Section>{summary}</Section>
      </Message>,
    );
    return `Displayed ${id}.`;
  },
});

Enable Channels JSX before using this example:

json
{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "@copilotkit/channels"
  }
}
<FrontendOnly frontend="slack">

The portable message is rendered as Slack Block Kit before Intelligence sends it to the conversation.

</FrontendOnly> <FrontendOnly frontend="teams">

The portable message is rendered as a Microsoft Teams Adaptive Card before Intelligence sends it to the conversation.

</FrontendOnly>

Where MCP belongs

Connect MCP servers to the selected agent backend. The Channels SDK forwards channel tools over AG-UI alongside the tools already exposed by that agent, so you do not need a second channel-specific MCP transport. Start with MCP tools, then use defineChannelTool only for actions that need the current channel conversation.

Continue with interactive messages and approvals, or use the Channel API reference.