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 services
  • 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 service so we can send commands through the event stream and receive responses as events.

Monitoring the Stream

Before we create the service, 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 Service

Now let’s spawn the MCP server as a service with duplex communication enabled:

Terminal window
r#'{
run: {|| npx -y "@modelcontextprotocol/server-everything" | lines },
duplex: true
}'# | .append mcp.spawn

This creates a service 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 service 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 service 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 Actor Chains

Create actors 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
.last 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
.last 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 actor will analyze.

This interactive exploration helps you understand the data structure before writing actors. Now let’s build an actor that watches for specific responses:

Terminal window
r#'{
run: {|frame, state|
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) {
{out: {message: "meaning of life detected!"}, next: $state}
} else {
{next: $state}
}
} else {
{next: $state}
}
}
}'# | .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 actor 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: Actors automatically respond to events as they flow through the stream
  • Composable: You can chain multiple actors together for complex automation
  • Persistent: Actors run continuously, maintaining state across interactions
  • Observable: All actor outputs become part of the permanent event stream

You could extend this pattern to create sophisticated automation - actors that trigger on specific responses, maintain counters, coordinate between multiple MCP servers, or trigger actions based on detected events.

Part 4: Advanced Operations

Now let’s explore advanced operational features for managing services and working with multiple servers.

Service Lifecycle Management

You can control the MCP server service lifecycle:

Terminal window
# Stop the service when done
.append mcp.terminate

In your monitoring window, you’ll see the lifecycle frames: mcp.terminate, mcp.stopped, and mcp.shutdown as the service 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 services:

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 services 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 service 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