Welcome back! In Chapter 2: Filesystem Navigation & Discovery, we gave our CLI a "flashlight" to see files on your local hard drive.
But modern development doesn't just happen on your hard drive. It happens in the cloud. It happens on Slack, GitHub, and Jira.
Imagine you want to send a message to a Slack channel via your CLI.
"Send update to #general"
A bad system forces you to know exactly what channels exist. If you type #gen, it shrugs. It doesn't know what's inside your Slack workspace because that data lives on a server far away.
We need a way to ask Slack: "Hey, do you have any channels starting with 'gen'?"
This is Remote Tool Integration using the Model Context Protocol (MCP).
Think of it like a Universal Remote Control. The remote (our CLI) doesn't know how to play a movie itself. But it knows exactly which signal to send to the DVD player (Slack) to make it happen, and it interprets the blinking light on the player to tell you it worked.
Input: User types #gen
Action: CLI talks to Slack via MCP.
Output: Suggestions: ['#general', '#genesis-project', '#gentle-reminders']
To achieve this, we rely on three steps:
Before we try to search, we check our connections. In our system, we maintain a list of clients (active connections). We look for one named "slack".
// Do we have a connection to a Slack MCP server?
function findSlackClient(clients) {
return clients.find(client =>
client.type === 'connected' &&
client.name.includes('slack')
)
}
If we are connected, we use callTool. This is the standard way to send a command via MCP. We don't need to know how Slack searches; we just need to know the tool name (slack_search_channels) and the arguments (query).
const result = await slackClient.client.callTool(
{
name: 'slack_search_channels',
arguments: {
query: 'gen', // The user's input
limit: 20,
},
},
)
Here is the tricky part. Remote tools often return data in formats designed for humans or LLMs to read, not strict JSON arrays.
Slack might return a string like this:
{"results": "Found 2 channels:\nName: #general\nName: #random"}
Or sometimes just Markdown:
Name: #general
Name: #random
We need a parser to strip away the wrapper and extract just the names.
// Regex to find lines starting with "Name: "
function parseChannels(text: string): string[] {
const channels = []
// Split by new line
for (const line of text.split('\n')) {
// Look for "Name: #some-channel"
const match = line.match(/^Name:\s*#?([a-z0-9_-]+)/)
if (match) {
channels.push(match[1]) // Add "some-channel" to list
}
}
return channels
}
Let's visualize the journey of a keystroke.
Let's look at slackChannelSuggestions.ts to see how we handle the messy reality of network requests.
Network requests are slow. If the user types #g, then #ge, then #gen very quickly, we might fire three requests. If the first one is still loading, we don't want to start a new one if we don't have to.
We use a variable inflightPromise to track an ongoing request.
let inflightQuery = null
let inflightPromise = null
// Inside the suggestion function...
if (inflightQuery === currentQuery) {
// If we are already asking about this query, wait for that same promise
return await inflightPromise
}
Slack's search engine is strict. If you search for a partial word with a hyphen like team-en, it might return nothing. It prefers whole words.
We add logic to strip the partial last segment to ensure we get results.
function mcpQueryFor(token: string) {
// If input is "team-en", slice it to "team"
const lastSep = token.lastIndexOf('-')
if (lastSep > 0) {
return token.slice(0, lastSep)
}
return token
}
Just like we cached filesystem results in Chapter 2: Filesystem Navigation & Discovery, we cache network results to avoid hitting API rate limits.
const cache = new Map<string, string[]>()
// After getting results from MCP:
cache.set(query, channels)
// Next time, check cache first:
if (cache.has(query)) {
return cache.get(query)
}
Note: We will explore advanced caching strategies in Chapter 6: Performance Caching Layer.
In this chapter, we learned how to bridge the gap between our local tool and the outside world.
Now our system can find commands (Chapter 1), find local files (Chapter 2), and find remote resources (Chapter 3).
But what if the user doesn't know what they are looking for yet? What if we could guess what they want based on what they did yesterday?
Next Chapter: History-Based Prediction
Generated by Code IQ