Skip to content

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:

Terminal window
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:

Terminal window
.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 closure
  • if $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-is
  • print ($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:

Terminal window
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 separate mcp.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:

Terminal window
{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:

  1. The mcp.send frame with your JSONRPC request
  2. 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:

Terminal window
{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:

Terminal window
{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:

Terminal window
.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:

Terminal window
.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:

Terminal window
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:

Terminal window
{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:

  1. The mcp.send frame with your math request
  2. The mcp.recv frame with “The sum of 21 and 21 is 42.”
  3. 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:

Terminal window
# 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:

Terminal window
# 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:

Terminal window
# Spawn a filesystem server
r#'{
run: {|| npx -y "@modelcontextprotocol/server-filesystem" /tmp | lines },
duplex: true
}'# | .append fs.spawn
# Spawn a memory server
r#'{
run: {|| npx -y "@modelcontextprotocol/server-memory" | lines },
duplex: true
}'# | .append memory.spawn

Now you can send commands to different servers:

Terminal window
# 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:

  1. Run MCP servers directly and communicate via JSONRPC
  2. Spawn MCP servers as cross.stream generators with duplex communication
  3. Send commands through the event stream using .append topic.send
  4. Receive responses as events using .cat --topic topic.recv
  5. Build workflows that leverage MCP capabilities through the stream
  6. 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