There’s an MCP server for almost everything these days but maybe you need one for an internal API. Or maybe you use a SAAS that thinks it’s fine to have your entire team (including non-technical) run bash commands, live in the terminal, and start using Claude Code. Yay.
The other option is standing up a custom MCP server that you can connect directly into Claude (the web version) and other platforms with a custom connector. The adoption path for your users then becomes a matter of just mentioning something that the MCP knows it can help with.
An MCP server is pretty simple. It’s a service that exposes a set of tools (create_document, list_users, etc). Each tool is a wrapper to an API call. Each tool and the server itself have written instructions(similar to a prompt) to help a model figure out when it could be useful.

(Diagram from the MCP docs site)
The MCP server can be setup to respond over stdio(when you just need it to run locally) or streamable HTTP(example.com/mcp, what this article is about). The MCP protocol has matured to a point that we have standard ways to call tools and standard ways to authenticate, there’s even a full Linux Foundation docs site for it.
The core pieces of standing up your own MCP server come down to:
1. A server that speaks MCP
Luckily there are plenty of options for pre-built MCP servers that you can run. I’ve been using ActionMCP(Ruby gem) to build out MCP servers, it’s a rails engine and you run it alongside your app but with its own rack process. All of the major languages have official MCP packages now that you can utilize, all following the standard MCP spec.
2. A list of tools
What will the MCP server do? Tools are just what they sound like and simple wrappers to the actual code for what it does. If you have an MCP server for a todo list app, here’s a few tools you’d have:
- add_todo
- delete_todo
- update_todo
- etc.
3. An authentication/registration layer
There are currently 3 standard ways to do this, I’m not going to cover these in this article.
Let’s build an MCP server
I found this list of free public APIs and there’s a Bored API, as in you ran out of things to do, ya know like the 90s. A simple API that returns activities when you’re bored. You can also ask for a particular category of activity such as education or busywork, great for when people want to learn something new(education) or I suppose just want to look busy(busywork).
Here’s all you need to know about the API. We’re just going to deal with the main endpoint, it returns a random activity.
GET https://bored-api.appbrewery.com/random
{
"activity": "Learn Express.js",
"availability": 0.25,
"type": "education",
"participants": 1,
"price": 0.1,
"accessibility": "Few to no challenges",
"duration": "hours",
"kidFriendly": true,
"link": "https://expressjs.com/",
"key": "3943506"
}
The idea is that if someone mentions they’re bored in conversation, you can cure their boredom via the MCP or at least give them an idea to. We can add instructions to the MCP server to try to help out if someone mentions bored or seems bored.
For example: “I’m bored, give me something to do”, “I’d like to learn something new, I’m bored in this meeting”.
A server that speaks MCP
I’m going to be adding this to an existing Rails app which I won’t be covering here. The reason for doing that is to make handling authentication later on with something like Devise or an external OAuth provider.
Within our app, let’s install ActionMCP.
bundle add actionmcp
bin/rails action_mcp:install:migrations
bin/rails generate action_mcp:install
bin/rails db:migrate
This adds the gem, adds a migration file(MCP client sessions), and then adds some scaffolding for running the MCP server.
The first thing we want to do is give our server some instructions, a simple prompt that helps the model figure out its purpose.
# ActionMCP Configuration
shared:
authentication: ["none"] # No authentication required by default
server_instructions: |
The Bored MCP server - Tools for suggesting activities when someone is bored.
Use random_activity for an open-ended suggestion.
There should be a short explanation of what the MCP server is for and then just an entry point explanation for a few of the tools. The tools themselves will have their own descriptions so you don’t want to get very detailed on individual tools. The agent will fetch a list of tools from your server and then be able to see the details of each tool that you’ve configured. Server instructions are also useful for any common conventions or patterns that are recommended for proper usage.
We can see what this looks like over the wire by booting the MCP server and sending a request to it.
$ bin/mcp
Starting ActionMCP server with Puma on port 62770...
(tip: add 'falcon' to your Gemfile for better concurrency)
=> Booting Puma
=> Rails 8.1.3.1 application starting in development
=> Run `bin/rails server --help` for more startup options
Puma starting in single mode...
* Puma version: 8.0.2 ("Into the Arena")
* Ruby version: ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +PRISM [arm64-darwin23]
* Min threads: 3
* Max threads: 3
* Environment: development
* PID: 24850
* Listening on http://127.0.0.1:62770
curl -s -D /dev/stderr -X POST http://127.0.0.1:62770/ \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-11-25",
"capabilities": {},
"clientInfo": { "name": "curl", "version": "1.0" }
}
}' | jq .
(protocolVersion is required for this)
HTTP/1.1 200 OK
mcp-session-id: a948058dc21b88bca67738a7bfc02f07
content-type: application/json
vary: Accept
content-length: 574
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2025-11-25",
"serverInfo": {
"name": "bored-mcp",
"version": "0.0.1"
},
"capabilities": {
"tools": {
"listChanged": false
},
"prompts": {
"listChanged": false
},
"logging": {},
"completions": {},
"resources": {
"subscribe": false,
"listChanged": false
}
},
"instructions": "The Bored MCP server - Tools for suggesting activities when someone is bored.\nUse random_activity for an open-ended suggestion"
}
}
Would ya look at that, our MCP server is alive and well. It has a name and the instructions that we wrote.
Let’s also add the MCP server to our local Claude Code via HTTP so we can interact with it. We can’t do anything with the MCP server yet since it doesn’t have any tools but soon it will.
$ claude mcp add --transport http bored-mcp http://localhost:62770 --scope user
$ claude mcp list
bored-mcp: http://localhost:62770 (HTTP) - ✔ Connected
A list of tools
In our server instructions we mentioned a random_activity tool, let’s write it.
ActionMCP has a generator for this which gives us the scaffolding for our tool, we’ll use that and fill in the details.
$ bin/rails generate action_mcp:tool random_activity
create app/mcp/tools/random_activity_tool.rb
class RandomActivityTool < ApplicationMCPTool
tool_name "random-activity"
description "Describe what this tool does"
property :input, type: "string", description: "Input", required: true
def perform
render(text: "Processing ")
rescue => e
report_error("Error: #{e.message}")
end
end
Let’s go ahead and add a description for what this tool does and then we can write the perform method which will be a simple fetch from the Bored API. Also, change the tool name to be snake_case which is the common convention.
require "net/http"
class RandomActivityTool < ApplicationMCPTool
tool_name "random_activity" # Changed to snake_case, the common convention
description "Suggests a random activity to do when you're bored"
def perform
# The Bored API endpoint mentioned previously
body = Net::HTTP.get(URI("https://bored-api.appbrewery.com/random"))
activity = JSON.parse(body)
# A render helper method to return the tool details
render(text: "#{activity['activity']} (#{activity['type']}, takes #{activity['duration']})")
end
end
ActionMCP has a few helper methods for the render portion of this, similar to Rails. You can use ActionMCP to render resources, images, audio, etc. For this tool, we’re just returning a simple string back to the model. As you build out your tools you’ll add more features like input params, a structured output schema, multiple render calls. This will do for now.
To be able to see what’s going on over the wire we have to do a bit more handshaking with the MCP server. MCP servers may use a session ID which you obtain by initializing a session with the server, you then use that session ID with each subsequent request. The session ID helps with replay requests, disconnections, and a few other things.
The flow for this handshake is:
- initialize - The request from earlier that returned our server instructions
- notifications/initialized - Let the MCP server know that we’ve initialized everything on our end, a simple ACK call
- tools/list - Ask for a list of tools that the MCP server supports
initialize
curl -s -D /dev/stderr -X POST http://127.0.0.1:62770/ \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-11-25",
"capabilities": {},
"clientInfo": { "name": "curl", "version": "1.0" }
}
}' | jq .
From that, you’ll save the session ID mcp-session-id: 498e3423d2c969721b6ed2fa76cc3085 so we can use it in the next two calls.
notifications/initialized
curl -s -X POST http://127.0.0.1:62770/ \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'Mcp-Session-Id: 498e3423d2c969721b6ed2fa76cc3085' \
-d '{ "jsonrpc": "2.0", "method": "notifications/initialized" }'
tools/list
curl -s -X POST http://127.0.0.1:62770/ \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'Mcp-Session-Id: 498e3423d2c969721b6ed2fa76cc3085' \
-d '{ "jsonrpc": "2.0", "id": 2, "method": "tools/list" }' | jq .
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"tools": [
{
"name": "random_activity",
"description": "Suggests a random activity to do when you're bored",
"inputSchema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {},
"additionalProperties": false
}
}
]
}
}Hey, there’s our tool, neat. So you can see what ActionMCP is doing to write out our random_activity tool as a standard MCP tool spec.
With that tool in place let’s see if Claude picks up on our request and uses our MCP server. We already configured our MCP server previously with claude mcp add --transport http bored-mcp http://localhost:62770 --scope user but run that to add it if you haven’t.

Here’s a sample with the prompt Today is boring, meh. The agent figured out that we seemed bored and reached out to the bored-mcp for help and found our boredom cure.
The random_activity tool then fetched a random activity from the Bored API which we returned as the text Make a couch fort (recreational, takes minutes) which came from our render call render(text: "#{activity['activity']} (#{activity['type']}, takes #{activity['duration']})") that we set as the response for our tool.
Next Steps
Our tool is very simple at the moment but you can see how we can use the tool’s perform method to do any additional work, API calls, etc. to provide a richer response back to the agent.
While the overall flow of an MCP server is pretty simple, we still didn’t get to deploying this and putting authentication in front of it both of which deserve their own posts.