Skip to content

Commit 5815716

Browse files
committed
refactor: move tools to mcp/tools folder
1 parent ac34540 commit 5815716

File tree

5 files changed

+237
-123
lines changed

5 files changed

+237
-123
lines changed

docs/architecture.md

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
- **UI**: React + Ink (terminal UI)
1111
- **State Management**: Easy-peasy
1212
- **Agent SDK**: Claude Agent SDK
13+
- **MCP SDK**: [Model Context Protocol TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk)
1314
- **Language**: TypeScript
1415

1516
### Core Components
@@ -224,18 +225,21 @@ The CLI can also run as an MCP server itself, exposing the agent as a tool to ot
224225

225226
**Shared MCP Infrastructure:**
226227

227-
- [src/mcp/utils/getMcpServer.ts](../src/mcp/utils/getMcpServer.ts) - MCP server factory
228+
- [src/mcp/getMcpServer.ts](../src/mcp/getMcpServer.ts) - MCP server factory
228229
- Creates `McpServer` instance
229-
- Registers `ask_agent` tool with Zod validation
230-
- Registers `get_agent_status` tool for initialization
230+
- Imports and registers tools from individual tool files
231231
- Shared by both stdio and HTTP modes
232-
- [src/mcp/utils/runStandaloneAgentLoop.ts](../src/mcp/utils/runStandaloneAgentLoop.ts) - Query execution
232+
- [src/mcp/tools/](../src/mcp/tools/) - Individual MCP tool definitions
233+
- [askAgent.ts](../src/mcp/tools/askAgent.ts) - `ask_agent` tool with Zod validation (general purpose)
234+
- [askAgentSlackbot.ts](../src/mcp/tools/askAgentSlackbot.ts) - `ask_agent_slackbot` tool with thread session support
235+
- [getAgentStatus.ts](../src/mcp/tools/getAgentStatus.ts) - `get_agent_status` tool for initialization
236+
- [src/mcp/runStandaloneAgentLoop.ts](../src/mcp/runStandaloneAgentLoop.ts) - Query execution
233237
- Uses shared `runAgentLoop()` from agent integration
234238
- Manages message queue and session state
235239
- Processes streaming responses with logging notifications
236240
- Implements `onServerConnection` callback for dynamic server selection
237241
- Returns final text response
238-
- [src/mcp/utils/getAgentStatus.ts](../src/mcp/utils/getAgentStatus.ts) - Status utility
242+
- [src/mcp/getAgentStatus.ts](../src/mcp/getAgentStatus.ts) - Status utility
239243
- Initializes agent to get available MCP servers
240244
- Called by `get_agent_status` tool on client initialization
241245
- Returns session ID and MCP servers list
@@ -358,6 +362,44 @@ Uses `cosmiconfig` for flexible configuration loading:
358362
}
359363
```
360364

365+
#### Specialized Subagents
366+
367+
The CLI supports specialized subagents for domain-specific tasks using the [Claude Subagent SDK](https://docs.claude.com/en/docs/claude-code/sub-agents). Subagents are defined in `agent-chat-cli.config.ts` and automatically invoked when user queries match their domain.
368+
369+
**Configuration:**
370+
371+
- [src/utils/createAgent.ts](../src/utils/createAgent.ts) - Subagent factory function
372+
- Creates subagent configuration with description, prompt, and MCP servers
373+
- Description used by routing agent for intelligent matching
374+
- Supports both local prompts (via `getPrompt`) and remote prompts (via `getRemotePrompt`)
375+
376+
**Example:**
377+
378+
```typescript
379+
agents: {
380+
"sales-partner-sentiment-agent": createAgent({
381+
description: "An expert SalesForce partner sentiment agent, designed to produce insights for renewal and churn conversations",
382+
prompt: getPrompt("agents/sales-partner-sentiment-agent.md"),
383+
mcpServers: ["salesforce"],
384+
}),
385+
}
386+
```
387+
388+
**Flow:**
389+
390+
1. User sends query (e.g., "Analyze partner churn")
391+
2. Routing agent matches query to subagent based on description
392+
3. Required MCP servers (e.g., `salesforce`) are automatically connected
393+
4. Subagent is invoked with specialized prompt and tools
394+
5. Response returned to user
395+
396+
**Benefits:**
397+
398+
- Domain-specific expertise with tailored prompts
399+
- Scoped MCP server access for security and performance
400+
- Automatic routing based on query intent
401+
- Supports dynamic prompt management via remote APIs
402+
361403
#### Tool Filtering
362404

363405
The `disallowedTools` configuration allows blocking specific MCP tools:

src/mcp/getMcpServer.ts

Lines changed: 20 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
2-
import { getAgentStatus } from "mcp/getAgentStatus"
3-
import { runStandaloneAgentLoop } from "mcp/runStandaloneAgentLoop"
4-
import { z } from "zod"
2+
import { registerAskAgentTool } from "mcp/tools/askAgent"
3+
import { registerAskAgentSlackbotTool } from "mcp/tools/askAgentSlackbot"
4+
import { registerGetAgentStatusTool } from "mcp/tools/getAgentStatus"
55

66
export const getMcpServer = () => {
77
// Store Claude Agent SDK sessionId per-instance (not shared across threads)
@@ -25,127 +25,29 @@ export const getMcpServer = () => {
2525
}
2626
)
2727

28-
mcpServer.registerTool(
29-
"ask_agent",
30-
{
31-
description:
32-
"Passes a query to the internal agent and returns its full output. The agent has access to configured MCP tools and will provide a complete response. DO NOT reprocess, analyze, or summarize the output - return it directly to the user as-is.",
33-
inputSchema: {
34-
query: z.string().min(1).describe("The query to send to the agent"),
28+
// Register tools
29+
registerAskAgentTool({
30+
mcpServer,
31+
context: {
32+
get sessionId() {
33+
return sessionId
3534
},
36-
},
37-
async ({ query }) => {
38-
const existingConnectedServers = sessionId
39-
? sessionConnectedServers.get(sessionId)
40-
: undefined
41-
42-
const { response, connectedServers } = await runStandaloneAgentLoop({
43-
prompt: query,
44-
mcpServer,
45-
sessionId,
46-
existingConnectedServers,
47-
onSessionIdReceived: (newSessionId) => {
48-
sessionId = newSessionId
49-
},
50-
})
51-
52-
// Update the session's connected servers
53-
if (sessionId) {
54-
sessionConnectedServers.set(sessionId, connectedServers)
55-
}
56-
57-
return {
58-
content: [
59-
{
60-
type: "text",
61-
text: response,
62-
},
63-
],
64-
}
65-
}
66-
)
67-
68-
mcpServer.registerTool(
69-
"ask_agent_slackbot",
70-
{
71-
description:
72-
"Slack bot integration tool. Passes a query to the internal agent and returns a response optimized for Slack. Supports per-thread session isolation.",
73-
inputSchema: {
74-
query: z
75-
.string()
76-
.min(1)
77-
.describe("The slack query to send to the agent"),
78-
systemPrompt: z
79-
.string()
80-
.optional()
81-
.describe("Optional additional system prompt to prepend"),
82-
threadId: z
83-
.string()
84-
.optional()
85-
.describe("Slack thread identifier for session isolation"),
35+
sessionConnectedServers,
36+
onSessionIdUpdate: (newSessionId) => {
37+
sessionId = newSessionId
8638
},
8739
},
88-
async ({ query, systemPrompt, threadId }) => {
89-
const existingSessionId = threadId
90-
? threadSessions.get(threadId)
91-
: undefined
40+
})
9241

93-
const existingConnectedServers = existingSessionId
94-
? sessionConnectedServers.get(existingSessionId)
95-
: undefined
96-
97-
const { response, connectedServers } = await runStandaloneAgentLoop({
98-
prompt: query,
99-
mcpServer,
100-
sessionId: existingSessionId,
101-
additionalSystemPrompt: systemPrompt,
102-
existingConnectedServers,
103-
onSessionIdReceived: (newSessionId) => {
104-
if (threadId) {
105-
threadSessions.set(threadId, newSessionId)
106-
}
107-
},
108-
})
109-
110-
// Update the session's connected servers
111-
if (existingSessionId || threadId) {
112-
const sessionId = existingSessionId || threadSessions.get(threadId!)
113-
if (sessionId) {
114-
sessionConnectedServers.set(sessionId, connectedServers)
115-
}
116-
}
117-
118-
return {
119-
content: [
120-
{
121-
type: "text",
122-
text: response,
123-
},
124-
],
125-
}
126-
}
127-
)
128-
129-
mcpServer.registerTool(
130-
"get_agent_status",
131-
{
132-
description:
133-
"Get the status of the agent including which MCP servers it has access to. Call this on initialization to see available servers.",
134-
inputSchema: {},
42+
registerAskAgentSlackbotTool({
43+
mcpServer,
44+
context: {
45+
threadSessions,
46+
sessionConnectedServers,
13547
},
136-
async () => {
137-
const status = await getAgentStatus(mcpServer)
48+
})
13849

139-
return {
140-
content: [
141-
{
142-
type: "text",
143-
text: JSON.stringify(status, null, 2),
144-
},
145-
],
146-
}
147-
}
148-
)
50+
registerGetAgentStatusTool({ mcpServer })
14951

15052
return mcpServer
15153
}

src/mcp/tools/askAgent.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
2+
import { runStandaloneAgentLoop } from "mcp/runStandaloneAgentLoop"
3+
import { z } from "zod"
4+
5+
export interface AskAgentContext {
6+
sessionId?: string
7+
sessionConnectedServers: Map<string, Set<string>>
8+
onSessionIdUpdate: (sessionId: string) => void
9+
}
10+
11+
export interface RegisterAskAgentToolProps {
12+
mcpServer: McpServer
13+
context: AskAgentContext
14+
}
15+
16+
export const registerAskAgentTool = ({
17+
mcpServer,
18+
context,
19+
}: RegisterAskAgentToolProps) => {
20+
mcpServer.registerTool(
21+
"ask_agent",
22+
{
23+
description:
24+
"Passes a query to the internal agent and returns its full output. The agent has access to configured MCP tools and will provide a complete response. DO NOT reprocess, analyze, or summarize the output - return it directly to the user as-is.",
25+
inputSchema: {
26+
query: z.string().min(1).describe("The query to send to the agent"),
27+
},
28+
},
29+
async ({ query }) => {
30+
const existingConnectedServers = context.sessionId
31+
? context.sessionConnectedServers.get(context.sessionId)
32+
: undefined
33+
34+
const { response, connectedServers } = await runStandaloneAgentLoop({
35+
prompt: query,
36+
mcpServer,
37+
sessionId: context.sessionId,
38+
existingConnectedServers,
39+
onSessionIdReceived: (newSessionId) => {
40+
context.onSessionIdUpdate(newSessionId)
41+
},
42+
})
43+
44+
// Update the session's connected servers
45+
if (context.sessionId) {
46+
context.sessionConnectedServers.set(context.sessionId, connectedServers)
47+
}
48+
49+
return {
50+
content: [
51+
{
52+
type: "text",
53+
text: response,
54+
},
55+
],
56+
}
57+
}
58+
)
59+
}

src/mcp/tools/askAgentSlackbot.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
2+
import { runStandaloneAgentLoop } from "mcp/runStandaloneAgentLoop"
3+
import { z } from "zod"
4+
5+
export interface AskAgentSlackbotContext {
6+
threadSessions: Map<string, string>
7+
sessionConnectedServers: Map<string, Set<string>>
8+
}
9+
10+
export interface RegisterAskAgentSlackbotToolProps {
11+
mcpServer: McpServer
12+
context: AskAgentSlackbotContext
13+
}
14+
15+
export const registerAskAgentSlackbotTool = ({
16+
mcpServer,
17+
context,
18+
}: RegisterAskAgentSlackbotToolProps) => {
19+
mcpServer.registerTool(
20+
"ask_agent_slackbot",
21+
{
22+
description:
23+
"Slack bot integration tool. Passes a query to the internal agent and returns a response optimized for Slack. Supports per-thread session isolation.",
24+
inputSchema: {
25+
query: z
26+
.string()
27+
.min(1)
28+
.describe("The slack query to send to the agent"),
29+
systemPrompt: z
30+
.string()
31+
.optional()
32+
.describe("Optional additional system prompt to prepend"),
33+
threadId: z
34+
.string()
35+
.optional()
36+
.describe("Slack thread identifier for session isolation"),
37+
},
38+
},
39+
async ({ query, systemPrompt, threadId }) => {
40+
const existingSessionId = threadId
41+
? context.threadSessions.get(threadId)
42+
: undefined
43+
44+
const existingConnectedServers = existingSessionId
45+
? context.sessionConnectedServers.get(existingSessionId)
46+
: undefined
47+
48+
const { response, connectedServers } = await runStandaloneAgentLoop({
49+
prompt: query,
50+
mcpServer,
51+
sessionId: existingSessionId,
52+
additionalSystemPrompt: systemPrompt,
53+
existingConnectedServers,
54+
onSessionIdReceived: (newSessionId) => {
55+
if (threadId) {
56+
context.threadSessions.set(threadId, newSessionId)
57+
}
58+
},
59+
})
60+
61+
// Update the session's connected servers
62+
if (existingSessionId || threadId) {
63+
const sessionId =
64+
existingSessionId || context.threadSessions.get(threadId!)
65+
if (sessionId) {
66+
context.sessionConnectedServers.set(sessionId, connectedServers)
67+
}
68+
}
69+
70+
return {
71+
content: [
72+
{
73+
type: "text",
74+
text: response,
75+
},
76+
],
77+
}
78+
}
79+
)
80+
}

0 commit comments

Comments
 (0)