Integrating MCP Servers
This tutorial teaches you how to interact with Model Context Protocol (MCP) servers, starting with direct terminal communication and then integrating through cross.stream’s event streaming system.
What You’ll Learn
- How to run MCP servers and communicate with them using JSONRPC
- How to spawn MCP servers as cross.stream generators
- How to send commands and receive responses through the event stream
Prerequisites
- Node.js installed on your system (in order to run the example MCP server)
- A running cross.stream store (see Installation)
Part 1: Direct MCP Server Communication
Let’s start by running a simple MCP server and poking at it directly with JSONRPC calls.
Starting the Server
Open a terminal and run:
npx -y "@modelcontextprotocol/server-everything"
You should see:
Starting default (STDIO) server...
The server is now running and listening for JSONRPC messages on stdin.
Discovering Available Tools
Let’s ask the server what tools it provides. Copy and paste this into the MCP server’s stdin and hit enter:
{"jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {}}
Press Enter. You’ll get back a response showing available tools, including an echo
tool that simply returns whatever message you send it.
Calling the Echo Tool
Now let’s call the echo tool. Copy and paste this into the MCP server’s stdin and hit enter:
{"jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": {"name": "echo", "arguments": {"message": "Hello from the terminal!"}}}
You should see a response like:
{"result": {"content": [{"type": "text", "text": "Hello from the terminal!"}], "isError": false}, "jsonrpc": "2.0", "id": 2}
The server successfully echoed back our message!
Trying the Math Tool
Now try the addition tool in the same terminal:
{"jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": {"name": "add", "arguments": {"a": 15, "b": 27}}}
You’ll get back:
{"result": {"content": [{"type": "text", "text": "42"}], "isError": false}, "jsonrpc": "2.0", "id": 3}
Press Ctrl+C to stop the server when you’re done exploring.
Part 2: Integration with cross.stream
Now let’s integrate this MCP server with cross.stream using a generator so we can send commands through the event stream and receive responses as events.
Monitoring the Stream
Before we create the generator, let’s open a new terminal window to monitor all activity on the stream. This will help us see everything that happens in real-time.
In a new terminal window, run:
.cat -f | each { if $in.hash != null { insert content { .cas $in.hash } } else { } | print ($in | table -e)}
This command:
.cat -f
- Reads the entire stream from the beginning and follows for new events (-f
for follow)| each { ... }
- For each frame in the stream, executes the closureif $in.hash != null { insert content { .cas $in.hash } } else { }
- If the frame has a hash, fetch and insert the actual content using.cas
, otherwise use the frame as-isprint ($in | table -e)
- Displays each frame in a nicely formatted table with expanded content
Keep this window open - you’ll see all the MCP server lifecycle events, requests, and responses flow through in real-time.
Creating the MCP Generator
Now let’s spawn the MCP server as a generator with duplex communication enabled:
r#'{ run: {|| npx -y "@modelcontextprotocol/server-everything" | lines }, duplex: true}'# | .append mcp.spawn
This creates a generator that:
- Runs the MCP server process (
npx -y "@modelcontextprotocol/server-everything"
) - Pipes output through
| lines
which provides “framing” - each line of output from the MCP server becomes a separatemcp.recv
event - Enables
duplex: true
so we can send input to the server’s stdin - Content from
mcp.send
frames is sent directly to the MCP server’s stdin (so JSONRPC messages must include newline endings)
In your monitoring window, you should see a mcp.spawn
and then a mcp.running
frame appear, indicating the generator has started successfully.
Sending Commands Through the Stream
Now we can send JSONRPC commands to the MCP server. First, let’s send a tools/list request:
{jsonrpc: "2.0", id: 1, method: "tools/list", params: {}} | to json -r | $in + "\n" | .append mcp.send
Receiving Responses
The generator will forward our request to the MCP server and stream the response back. In your monitoring window, you should see:
- The
mcp.send
frame with your JSONRPC request - The
mcp.recv
frame with the server’s response containing the tools list
You’ll see the same tools list response we got earlier, but now it’s flowing through the event stream as structured frames.
Sending Echo Commands
Let’s try the echo command through the stream:
{jsonrpc: "2.0", id: 2, method: "tools/call", params: {name: "echo", arguments: {message: "Hello from cross.stream!"}}} | to json -r | $in + "\n" | .append mcp.send
Watch your monitoring window - you’ll see the echo response come back in a mcp.recv
frame.
Math Through the Stream
Try the addition tool:
{jsonrpc: "2.0", id: 3, method: "tools/call", params: {name: "add", arguments: {a: 100, b: 200}}} | to json -r | $in + "\n" | .append mcp.send
The MCP server responds with “The sum of 100 and 200 is 300.” which will appear in your monitoring window as a mcp.recv
frame.
Part 3: Building Event-Driven Workflows
Now that we have MCP server responses flowing through cross.stream, we can build reactive workflows that automatically respond to events.
Building Handler Chains
Create handlers that automatically react to MCP responses and trigger follow-up actions. But first, let’s explore the structure of MCP responses interactively to understand what we’re working with.
Exploring Response Structure
Let’s examine the most recent MCP response from our math operation:
.head mcp.recv | .cas $in.hash | from json
This shows you the full JSONRPC response structure. Now let’s drill down to extract just the text content:
.head mcp.recv | .cas $in.hash | from json | get result.content.0.text
You should see “The sum of 100 and 200 is 300.” - this is the actual text our handler will analyze.
This interactive exploration helps you understand the data structure before writing handlers. Now let’s build a handler that watches for specific responses:
r#'{ run: {|frame| if ($frame.topic == "mcp.recv") { let content = .cas $frame.hash | from json | get -i result.content.0.text if ($content != null and "42" in $content) { "meaning of life detected!" } } }}'# | .append meaning.detector.register
Now let’s test it by sending a calculation that equals 42:
{jsonrpc: "2.0", id: 10, method: "tools/call", params: {name: "add", arguments: {a: 21, b: 21}}} | to json -r | $in + "\n" | .append mcp.send
Watch your monitoring window! You’ll see:
- The
mcp.send
frame with your math request - The
mcp.recv
frame with “The sum of 21 and 21 is 42.” - A
meaning.detector.out
frame containing “meaning of life detected!”
The handler uses simple substring matching ("42" in $content
) to scan response text. When the target value is found, it returns “meaning of life detected!” which automatically becomes a new frame in the stream.
This demonstrates the power of event-driven workflows in cross.stream:
- Reactive: Handlers automatically respond to events as they flow through the stream
- Composable: You can chain multiple handlers together for complex automation
- Persistent: Handlers run continuously, maintaining state across interactions
- Observable: All handler outputs become part of the permanent event stream
You could extend this pattern to create sophisticated automation - handlers that trigger on specific responses, maintain counters, coordinate between multiple MCP servers, or trigger commands based on detected events.
Part 4: Advanced Operations
Now let’s explore advanced operational features for managing generators and working with multiple servers.
Generator Lifecycle Management
You can control the MCP server generator lifecycle:
# Stop the generator when done.append mcp.terminate
In your monitoring window, you’ll see the lifecycle frames: mcp.terminate
, mcp.stopped
, and mcp.shutdown
as the generator shuts down completely.
Logging and Monitoring
All MCP interactions are now part of your event stream, so you can:
# Query all MCP-related events.cat | where { $in.topic | str starts-with "mcp." }
Multiple MCP Servers
You can run multiple MCP servers as separate generators:
# Spawn a filesystem serverr#'{ run: {|| npx -y "@modelcontextprotocol/server-filesystem" /tmp | lines }, duplex: true}'# | .append fs.spawn
# Spawn a memory serverr#'{ run: {|| npx -y "@modelcontextprotocol/server-memory" | lines }, duplex: true}'# | .append memory.spawn
Now you can send commands to different servers:
# Send to filesystem server{jsonrpc: "2.0", id: 4, method: "tools/list", params: {}} | to json -r | $in + "\n" | .append fs.send
# Send to memory server{jsonrpc: "2.0", id: 5, method: "tools/list", params: {}} | to json -r | $in + "\n" | .append memory.send
Summary
You’ve learned how to:
- Run MCP servers directly and communicate via JSONRPC
- Spawn MCP servers as cross.stream generators with duplex communication
- Send commands through the event stream using
.append topic.send
- Receive responses as events using
.cat --topic topic.recv
- Build workflows that leverage MCP capabilities through the stream
- Manage generator lifecycle with start, stop, and monitoring
This pattern lets you integrate any MCP-compatible tool into your cross.stream workflows while maintaining a complete audit trail of all interactions.
Next Steps
- Explore the generators reference for advanced generator configuration
- Learn about handlers reference for processing MCP responses
- Learn about topic suffixes for understanding the event lifecycle
- Try spawning other MCP servers from the official examples