Skip to content
Merged
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
7 changes: 5 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Anthropic API Key
# Required
ANTHROPIC_API_KEY=

# GitHub Access Token
ENABLE_LOGGING=false

# Generate at https://github.com/settings/personal-access-tokens
GITHUB_ACCESS_TOKEN=

5 changes: 5 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules/
dist/
*.env
*.env.example
.prettierignore
9 changes: 6 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
---

Default to using Bun instead of Node.js.

- Use `bun <file>` instead of `node <file>` or `ts-node <file>`
Expand All @@ -17,7 +15,12 @@ Default to using Bun instead of Node.js.
- Prefer named exports vs default exports
- Prefer try/catch over vanilla promises
- If a function has two or more arguments, use an object. And use an interface, vs adding types inline.
- Never use implicit returns in react components. They should ONLY be used in one-liners.-
- Never use implicit returns in react components. They should ONLY be used in one-liners.

## Common Developer Commands

- `bun test`
- `bun type-check`

## APIs

Expand Down
148 changes: 137 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# Agent Chat CLI
# Agent Chat Cli

A bare-bones, terminal-based chat CLI built to explore the new [Claude Agent SDK](https://docs.claude.com/en/api/agent-sdk/overview). Terminal rendering is built on top of [React Ink](https://github.com/vadimdemedes/ink).
A minimalist, terminal-based chat CLI built to explore the new [Claude Agent SDK](https://docs.claude.com/en/api/agent-sdk/overview) and based on [damassi/agent-chat-cli](https://github.com/damassi/agent-chat-cli). Terminal rendering is built on top of [React Ink](https://github.com/vadimdemedes/ink).

Additionally, via inference, Agent Chat CLI supports lazy, turn-based MCP connections to keep token costs down. The agent will only use those MCP servers you ask about, limiting the context that is sent up to the LLM. (After an MCP server is connected it remains connected, however.)

## Overview

The app has three modes:

Expand All @@ -12,7 +16,7 @@ The agent, including MCP server setup, is configured in [agent-chat-cli.config.t

The MCP _client_ is configured in [mcp-client.config.ts](mcp-client.config.ts).

https://github.com/user-attachments/assets/c2026c47-c798-4a1d-a68a-54e4abe73c63
https://github.com/user-attachments/assets/f9a82631-ee26-4a7b-9d89-a732d2605513

### Why?

Expand Down Expand Up @@ -49,6 +53,18 @@ OAuth support works out of the box via `mcp-remote`:

See the config above for an example.

### Example MCP Servers

For demonstration purposes, Agent is configured with the following MCP servers:

- **Chrome DevTools MCP**: https://developer.chrome.com/blog/chrome-devtools-mcp
- **Github MCP**: https://github.com/github/github-mcp-server
- [Generate a Github PAT token](https://github.com/settings/personal-access-tokens)
- **Notion MCP**: https://developers.notion.com/docs/mcp
- Authenticate via OAuth, which will launch a browser when attempting to connect

**Note**: OAuth-based MCP servers (Notion, JIRA, etc) require browser-based authentication and cannot be deployed remotely. These servers are only accessible in the CLI version of the agent.

### Usage

#### Interactive Agent Mode
Expand Down Expand Up @@ -80,11 +96,11 @@ Configure the MCP server connection in `mcp-client.config.ts`. HTTP is also supp
Run as a stand-alone MCP server, using one of two modes:

```bash
bun server
bun server:http
bun server:http # streaming HTTP (use this for deployments)
bun server # stdio
```

The server exposes an `ask_agent` tool that other MCP clients can use to interact with the agent. The agent has access to all configured MCP servers and can use their tools.
The MCP server exposes an `ask_agent` and `ask_agent_slackbot` tools that other MCP clients can use to interact with the agent. The agent has access to all configured MCP servers and can use their tools.

### Configuration

Expand All @@ -96,31 +112,141 @@ To add specific instructions for each MCP server, create a markdown file in `src

```ts
const config = {
systemPrompt: "You are a helpful agent."
systemPrompt: getPrompt("system.md"),
mcpServers: {
someMcpServer: {
command: "npx",
command: "bunx",
args: ["..."],
prompt: getPrompt("someMcpServer.md"),
},
},
}
```

#### Remote Prompts

Prompts can be loaded from remote sources (e.g., APIs) using `getRemotePrompt`. This enables dynamic prompt management where prompts are stored in a database or CMS rather than in files.

Both `getPrompt` (for local files) and `getRemotePrompt` (for API calls) return lazy functions that are only evaluated when the agent needs them, ensuring prompts are fetched on-demand during each LLM turn, enabling iteration in real time.

```ts
import { getRemotePrompt } from "./src/utils/getRemotePrompt"

const config = {
systemPrompt: getRemotePrompt(),
mcpServers: {
someMcpServer: {
command: "bunx",
args: ["..."],
prompt: getRemotePrompt({
fetchPrompt: async () => {
const response = await fetch("https://some-prompt/name")

if (!response.ok) {
throw new Error(
`[agent] [getRemotePrompt] [ERROR HTTP] status: ${response.status}`
)
}

const text = await response.text()
return text
},
}),
},
},
}
```

You can also provide a fallback to a local file if the remote fetch fails:

```ts
const config = {
mcpServers: {
github: {
prompt: getRemotePrompt({
fallback: "github.md"
fetchPrompt: ...
}),
},
},
}
```

#### Denying Tools

You can prevent specific MCP tools from being used by adding a `denyTools` array to your server configuration:
You can limit what tools the claude-agent-sdk has access to by adding a `disallowedTools` config:

```ts
const config = {
disallowedTools: ["Bash"],
}
```

You can also prevent specific MCP tools from being used by adding a `disallowedTools` array to your server configuration:

```ts
const config = {
mcpServers: {
github: {
command: "npx",
command: "bunx",
args: ["..."],
denyTools: ["delete_repository", "update_secrets"],
disallowedTools: ["delete_repository", "update_secrets"],
},
},
}
```

Denied tools are filtered at the SDK level and won't be available to the agent.

In CLI mode, if `permissionMode` is set to "ask" then a prompt will appear to confirm when tools need to be invoked.

### Specialized Subagents

You can define specialized subagents in `agent-chat-cli.config.ts` to handle domain-specific tasks, leveraging the powerful [Claude Subagent SDK](https://docs.claude.com/en/docs/claude-code/sub-agents). Subagents are automatically invoked when user queries match their domain, and they have access to specific MCP servers.

#### Example

```ts
import { createAgent } from "./src/utils/createAgent"
import { getPrompt } from "./src/utils/getPrompt"

const config = {
agents: {
"sales-partner-sentiment-agent": createAgent({
description:
"An expert SalesForce partner sentiment agent, designed to produce insights for renewal and churn conversations",
prompt: getPrompt("agents/sales-partner-sentiment-agent.md"),
mcpServers: ["salesforce"],
}),
},
mcpServers: {
salesforce: {
description: "Salesforce CRM: leads, opportunities, accounts...",
command: "bunx",
args: ["-y", "@tsmztech/[email protected]"],
enabled: true,
},
},
}
```

When a user asks something like "Analyze partner churn", the routing agent will:

1. Match the query to the `sales-partner-sentiment-agent` based on its description
2. Automatically connect to the required `salesforce` MCP server
3. Invoke the subagent with its specialized prompt and tools

The `description` field is **critical**; it's used by the routing agent to determine when to invoke the subagent.

**Note:** Subagents also support remote prompts via `getRemotePrompt`, allowing you to manage agent prompts dynamically from an API or database.

### Note on Lazy MCP Server Initialization

In order to keep LLM costs low and response times quick, a specialized sub-agent sits in front of user queries to infer which MCP servers are needed; the result is then forwarded on to the main agent, lazily initializing required MCP servers. Without this, we would need to initialize _all_ MCP servers defined in the config upfront, and for every query that we send to Anthropic, we'd _also_ be sending along a huge system prompt, and this is very expensive!

#### The Flow

- User sends a message, something like "In Salesforce, tell me about some recent leads"
- Sub-agent forwards message onto Anthropic's light-weight Haiku model and asks which MCP servers seem to be necessary
- Returns result as JSON, and based on the result, mcpServers are passed to the main agent query
- Agent now boots quickly and responds in a timely way, vs having to wait for every MCP server to initialize before being able to chat
63 changes: 56 additions & 7 deletions agent-chat-cli.config.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,80 @@
import type { AgentChatConfig } from "./src/store"
import { createAgent } from "./src/utils/createAgent"
import { getPrompt } from "./src/utils/getPrompt"

const config: AgentChatConfig = {
systemPrompt: getPrompt("system.md"),
model: "sonnet",
stream: true,

agents: {
"demo-agent": createAgent({
description: "A claude subagent designed to show off functionality",
prompt: getPrompt("agents/demo-agent.md"),
mcpServers: [],
}),
},

mcpServers: {
chrome: {
description:
"The Chrome DevTools MCP server adds web browser automation and debugging capabilities to your AI agent",
command: "bunx",
args: ["chrome-devtools-mcp@latest"],
},
github: {
description:
"GitHub MCP tools to search code, PRs, issues; discover documentation in repo docs/; find deployment guides and code examples.",
prompt: getPrompt("github.md"),
command: "npx",
command: "bunx",
args: [
"[email protected]",
"https://api.githubcopilot.com/mcp/readonly",
"--header",
`Authorization: Bearer ${process.env.GITHUB_ACCESS_TOKEN}`,
],
env: {
GITHUB_ACCESS_TOKEN: process.env.GITHUB_ACCESS_TOKEN!,
},
denyTools: [],
disallowedTools: [],
enabled: true,
},

notion: {
description:
"Notion workspace for documentation, wikis, OKRs, department pages, onboarding guides. Navigate hierarchies, search pages, retrieve structured content.",
prompt: getPrompt("notion.md"),
command: "npx",
command: "bunx",
args: ["[email protected]", "https://mcp.notion.com/mcp"],
enabled: true,
},

/**
// Example of how to use getRemotePrompt

someOtherServer: {
description: "Some description",
command: "bunx",
args: ["[email protected]", "https://mcp.some-server.com/mcp"],
prompt: getRemotePrompt({
fetchPrompt: async () => {
const response = await fetch("https://some-prompt/name")

if (!response.ok) {
throw new Error(
`[agent] [getRemotePrompt] [ERROR HTTP] status: ${response.status}`
)
}

const text = await response.text()
return text
},
}),
},
*/
},

disallowedTools: ["Bash"],

// Ungate MCP tools, which have their own disallowedTools array.
permissionMode: "bypassPermissions",
stream: false,
}

export default config
6 changes: 3 additions & 3 deletions bun.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "artsy-agent-claude",
"name": "agent-claude",
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.1.1",
"@anthropic-ai/claude-agent-sdk": "^0.1.30",
"@modelcontextprotocol/sdk": "^1.18.2",
"cors": "^2.8.5",
"cosmiconfig": "^9.0.0",
Expand Down Expand Up @@ -35,7 +35,7 @@
"packages": {
"@alcalzone/ansi-tokenize": ["@alcalzone/[email protected]", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-qI/5TaaaCZE4yeSZ83lu0+xi1r88JSxUjnH4OP/iZF7+KKZ75u3ee5isd0LxX+6N8U0npL61YrpbthILHB6BnA=="],

"@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/[email protected].1", "", { "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.33.5", "@img/sharp-darwin-x64": "^0.33.5", "@img/sharp-linux-arm": "^0.33.5", "@img/sharp-linux-arm64": "^0.33.5", "@img/sharp-linux-x64": "^0.33.5", "@img/sharp-win32-x64": "^0.33.5" } }, "sha512-+12GQktMFc5Uqz6oVjJbj7Q+GD5QDorKEKtInALKD7VleJwLlFbMYIlm4586owIV5veFvb6bAVofKn9CnYWtvw=="],
"@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/[email protected].30", "", { "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.33.5", "@img/sharp-darwin-x64": "^0.33.5", "@img/sharp-linux-arm": "^0.33.5", "@img/sharp-linux-arm64": "^0.33.5", "@img/sharp-linux-x64": "^0.33.5", "@img/sharp-win32-x64": "^0.33.5" }, "peerDependencies": { "zod": "^3.24.1" } }, "sha512-lo1tqxCr2vygagFp6kUMHKSN6AAWlULCskwGKtLB/JcIXy/8H8GsLSKX54anTsvc9mBbCR8wWASdFmiiL9NSKA=="],

"@babel/code-frame": ["@babel/[email protected]", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],

Expand Down
Loading