Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/add-mcp-client-support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"chat": minor
---

Add MCP (Model Context Protocol) client support for tool discovery and invocation from remote MCP servers. Configure via `mcpServers` in `ChatConfig` and access tools through `chat.mcp.listTools()` and `chat.mcp.callTool()`.
44 changes: 44 additions & 0 deletions apps/docs/content/docs/api/chat.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ const bot = new Chat(config);
type: 'number',
default: '500',
},
mcpServers: {
description: 'MCP server configurations for tool discovery and invocation. Requires @modelcontextprotocol/sdk.',
type: 'McpServerConfig[]',
default: 'undefined',
},
}}
/>

Expand Down Expand Up @@ -407,6 +412,45 @@ bot.onAppHomeOpened(async (event) => {
}}
/>

## mcp

The `McpManager` instance for interacting with connected MCP servers. Returns a `NoopMcpManager` if no `mcpServers` were configured.

```typescript
// List all available tools
const tools = await bot.mcp.listTools();

// Call a tool
const result = await bot.mcp.callTool("search_issues", { query: "error" });

// Call a tool with per-call auth headers
const result = await bot.mcp.callTool(
"search_issues",
{ query: "error" },
{ headers: { Authorization: `Bearer ${userToken}` } }
);

// Refresh tool lists from all servers
await bot.mcp.refresh();
```

<TypeTable
type={{
'listTools()': {
description: 'Returns all tools from connected MCP servers.',
type: 'Promise<McpTool[]>',
},
'callTool(name, args?, options?)': {
description: 'Invoke a tool by name. Pass options.headers to override auth for this call.',
type: 'Promise<McpToolResult>',
},
'refresh()': {
description: 'Re-fetch tool lists from all connected servers.',
type: 'Promise<void>',
},
}}
/>

## Utility methods

### webhooks
Expand Down
181 changes: 181 additions & 0 deletions apps/docs/content/docs/mcp.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
---
title: MCP (Model Context Protocol)
description: Discover and invoke tools from remote MCP servers directly from your chat bot handlers.
type: guide
prerequisites:
- /docs/usage
---

Chat SDK can connect to remote [MCP](https://modelcontextprotocol.io/) servers to discover and invoke tools. This lets your bot call APIs like Sentry, Linear, Notion, and Figma through a standard protocol.

## Installation

The MCP SDK is an optional peer dependency:

```bash
pnpm add @modelcontextprotocol/sdk
```

## Configuration

Pass `mcpServers` when creating your `Chat` instance:

```typescript title="lib/bot.ts" lineNumbers
import { Chat } from "chat";

const bot = new Chat({
userName: "mybot",
adapters: { slack },
state,
mcpServers: [
{
name: "sentry",
transport: {
type: "http",
url: "https://mcp.sentry.dev/mcp",
headers: {
Authorization: `Bearer ${process.env.SENTRY_AUTH_TOKEN}`,
},
},
},
],
});
```

MCP servers are connected lazily on the first webhook request.

## Listing tools

Discover all tools available across connected servers:

```typescript title="lib/bot.ts" lineNumbers
const tools = await bot.mcp.listTools();

for (const tool of tools) {
console.log(`${tool.serverName}/${tool.name}: ${tool.description}`);
}
```

Each tool includes `name`, `description`, `inputSchema` (JSON Schema), and `serverName`.

## Calling tools

Invoke a tool by name from any handler:

```typescript title="lib/bot.ts" lineNumbers
bot.onNewMention(async (thread, message) => {
const result = await bot.mcp.callTool("search_issues", {
query: message.text,
});

const text = result.content
.filter((c) => c.type === "text")
.map((c) => c.text)
.join("\n");

await thread.post(text);
});
```

## Transport types

| Type | Config value | Description |
|------|-------------|-------------|
| Streamable HTTP | `"http"` | Default. Modern bidirectional transport over HTTP. |
| Server-Sent Events | `"sse"` | Legacy SSE-based transport. Use if the server doesn't support Streamable HTTP. |

## Authentication

### Service-level (static headers)

The bot uses a shared API key for all requests. Configure headers directly in the transport:

```typescript
mcpServers: [
{
name: "sentry",
transport: {
type: "http",
url: "https://mcp.sentry.dev/mcp",
headers: { Authorization: `Bearer ${process.env.SENTRY_AUTH_TOKEN}` },
},
},
];
```

### Per-tenant

Look up the tenant's token at call time and pass it via the `headers` option. A temporary connection is created with those headers for the duration of the call:

```typescript title="lib/bot.ts" lineNumbers
bot.onNewMention(async (thread, message) => {
const tenantToken = await lookupTenantToken(thread.id);

const result = await bot.mcp.callTool(
"search_issues",
{ query: message.text },
{ headers: { Authorization: `Bearer ${tenantToken}` } }
);

await thread.post(result.content[0].text);
});
```

### Per-user

Same pattern — resolve the user's OAuth token and pass it per-call:

```typescript title="lib/bot.ts" lineNumbers
bot.onNewMention(async (thread, message) => {
const userToken = await getUserOAuthToken(message.author.id);

const result = await bot.mcp.callTool(
"create_issue",
{ title: message.text },
{ headers: { Authorization: `Bearer ${userToken}` } }
);

await thread.post(`Created: ${result.content[0].text}`);
});
```

## Multiple servers

Tools from all configured servers are merged. The SDK routes each `callTool` to the correct server automatically:

```typescript
mcpServers: [
{
name: "sentry",
transport: { type: "http", url: "https://mcp.sentry.dev/mcp" },
},
{
name: "linear",
transport: {
type: "http",
url: "https://mcp.linear.app/mcp",
headers: { Authorization: `Bearer ${process.env.LINEAR_API_KEY}` },
},
},
];
```

## Refreshing tools

If a server's tool list changes at runtime, re-fetch it:

```typescript
await bot.mcp.refresh();
```

## Graceful degradation

If a server fails to connect during initialization, a warning is logged and the remaining servers continue normally. Your bot won't crash because one MCP server is down.

## Error codes

| Code | When |
|------|------|
| `MCP_TOOL_NOT_FOUND` | `callTool` is called with a tool name that doesn't exist on any connected server. |
| `MCP_NOT_CONFIGURED` | `callTool` is called but no `mcpServers` were configured. |
| `MCP_SDK_NOT_INSTALLED` | `@modelcontextprotocol/sdk` is not installed. |
1 change: 1 addition & 0 deletions apps/docs/content/docs/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"posting-messages",
"---Features---",
"...",
"mcp",
"error-handling",
"---Platform Adapters---",
"...adapters",
Expand Down
9 changes: 9 additions & 0 deletions packages/chat/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"unified": "^11.0.5"
},
"devDependencies": {
"@modelcontextprotocol/sdk": "^1.27.1",
"@types/mdast": "^4.0.4",
"@types/node": "^22.10.2",
"tsup": "^8.3.5",
Expand All @@ -67,5 +68,13 @@
"google-chat",
"bot"
],
"peerDependencies": {
"@modelcontextprotocol/sdk": "^1.15.0"
},
"peerDependenciesMeta": {
"@modelcontextprotocol/sdk": {
"optional": true
}
},
"license": "MIT"
}
21 changes: 21 additions & 0 deletions packages/chat/src/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
setChatSingleton,
} from "./chat-singleton";
import { isJSX, toModalElement } from "./jsx-runtime";
import { McpClientManager, NoopMcpManager } from "./mcp";
import type { McpManager } from "./mcp-types";
import { Message, type SerializedMessage } from "./message";
import type { ModalElement } from "./modals";
import { type SerializedThread, ThreadImpl } from "./thread";
Expand Down Expand Up @@ -181,6 +183,7 @@ export class Chat<
private readonly logger: Logger;
private readonly _streamingUpdateIntervalMs: number;
private readonly _dedupeTtlMs: number;
private readonly mcpManager: McpManager;

private readonly mentionHandlers: MentionHandler<TState>[] = [];
private readonly messagePatterns: MessagePattern<TState>[] = [];
Expand Down Expand Up @@ -208,6 +211,11 @@ export class Chat<
*/
readonly webhooks: Webhooks<TAdapters>;

/** MCP manager for tool discovery and invocation. */
get mcp(): McpManager {
return this.mcpManager;
}

constructor(config: ChatConfig<TAdapters>) {
this.userName = config.userName;
this._stateAdapter = config.state;
Expand All @@ -232,6 +240,11 @@ export class Chat<
}
this.webhooks = webhooks as Webhooks<TAdapters>;

// Initialize MCP manager
this.mcpManager = config.mcpServers?.length
? new McpClientManager(config.mcpServers, this.logger)
: new NoopMcpManager();

this.logger.debug("Chat instance created", {
adapters: Object.keys(config.adapters),
});
Expand Down Expand Up @@ -289,6 +302,11 @@ export class Chat<
);
await Promise.all(initPromises);

// Initialize MCP servers (if configured)
if (this.mcpManager instanceof McpClientManager) {
await this.mcpManager.initialize();
}

this.initialized = true;
this.logger.info("Chat instance initialized", {
adapters: Array.from(this.adapters.keys()),
Expand All @@ -300,6 +318,9 @@ export class Chat<
*/
async shutdown(): Promise<void> {
this.logger.info("Shutting down chat instance...");
if (this.mcpManager instanceof McpClientManager) {
await this.mcpManager.close();
}
await this._stateAdapter.disconnect();
this.initialized = false;
this.initPromise = null;
Expand Down
11 changes: 11 additions & 0 deletions packages/chat/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,17 @@ export {
toPlainText,
walkAst,
} from "./markdown";
// MCP types
export type {
McpCallToolOptions,
McpContentBlock,
McpHeaders,
McpManager,
McpServerConfig,
McpTool,
McpToolResult,
McpTransportConfig,
} from "./mcp-types";
// Modal types
export type {
ModalChild,
Expand Down
Loading