Skip to content

Connecting to the MCP Server

This recipe connects to a pgEdge Postgres MCP Server in the way that any MCP client does: it checks that the server is running, introduces itself with the initialize handshake, and shows what the server does with a token that it does not know. The checks suit a deployment script, or a first test of a new server and token. Each step shows the code in every supported language; selecting a language tab switches every tab on the site to that language.

These examples are for pgEdge Postgres MCP Server 1.1.0.

The program prints the following output:

Health: ok
Server: pgedge-postgres-mcp 1.1.0
Protocol version: 2025-06-18
Wrong token: HTTP 401, Invalid or unknown token

Preparing the Ingredients

The examples need a running pgEdge Postgres MCP Server in HTTP mode with authentication turned on, and an API token that it accepts. The scripts/start_postgres_mcp.sh script in the cookbook repository starts a test server in Docker. The script creates a cookbook_mcp schema in the cookbook test PostgreSQL server (which scripts/start_test_server.sh starts) holding a products table of made-up products, with an embedding of each product's description made by Ollama with the nomic-embed-text model. It then generates a token, starts the server, and prints the MCP_URL and MCP_TOKEN values that the examples read from the environment.

The server speaks the Model Context Protocol (MCP), which is built on JSON-RPC 2.0. A client sends each request as a POST to /mcp/v1 with the token in an Authorization: Bearer header, and the server replies with a JSON object that holds either a result or an error. The server replies with HTTP 200 to a JSON-RPC error as well as to a success, so each program checks the reply for an error member as well as the HTTP status. No program prints the token.

The following table lists the HTTP client that each example uses:

Language HTTP client
Python httpx
TypeScript The fetch function that Node.js provides
Java The java.net.http package, with Jackson for JSON
C# The HttpClient class and System.Text.Json, which .NET provides
Go The net/http package
Rust reqwest, with serde_json for JSON
Ruby Net::HTTP and json from the standard library
curl curl and jq

The following tabs show the dependency declaration from each example project; the TypeScript project needs only the compiler and the Node.js type definitions, the Java project adds Jackson, the Rust project adds reqwest and serde_json, and the C#, Go and Ruby projects need nothing beyond their standard libraries:

# httpx, an HTTP client with a requests-like API.
httpx>=0.28,<1
{
  "name": "connect",
  "private": true,
  "type": "module",
  "scripts": {
    "build": "tsc",
    "start": "node dist/main.js"
  },
  "devDependencies": {
    "@types/node": "^22.0.0",
    "typescript": "^5.9.0"
  }
}
<dependencies>
  <!-- Jackson, for reading and writing JSON; the HTTP client is part of
       the JDK. -->
  <dependency>
    <groupId>tools.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>3.2.4</version>
  </dependency>
</dependencies>
<!-- HttpClient and System.Text.Json are part of .NET, so the project
     needs no packages. -->
// The program uses only the standard library, so there is nothing to add.
[dependencies]
# The blocking client, with TLS from rustls and request and response bodies
# as JSON; the default features add HTTP/2 and other things this example
# does not use.
reqwest = { version = "0.13", default-features = false, features = ["blocking", "json", "rustls"] }
serde_json = "1"
source "https://rubygems.org"

# The program uses only Net::HTTP and json from the standard library, so
# there is no gem to add.

Importing the Libraries

Each program begins by importing the HTTP client, along with any standard library modules that it uses. The TypeScript program imports nothing, because Node.js provides fetch as a global, and the curl script checks that curl and jq are installed. The following tabs show the imports for each language:

import os
import sys

import httpx
// Node.js provides fetch and AbortSignal as globals, so this program
// imports nothing.
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpTimeoutException;
import java.time.Duration;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import tools.jackson.core.JacksonException;
import tools.jackson.databind.JsonNode;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.json.JsonMapper;
using System.Net;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
import (
    "bytes"
    "encoding/json"
    "errors"
    "fmt"
    "io"
    "net/http"
    "os"
    "strings"
    "time"
)
use std::env;
use std::error::Error;
use std::process::ExitCode;
use std::time::Duration;

use reqwest::blocking::Client;
use serde_json::{json, Value};
require "json"
require "net/http"
require "uri"
# Stop at the first failed command or unset variable, and treat a failure
# anywhere in a pipeline as a failure of the whole pipeline.
set -euo pipefail

# The script needs curl to make the requests and jq to build the requests
# and read the replies.
for tool in curl jq; do
    command -v "$tool" >/dev/null || { echo "$tool is required" >&2; exit 1; }
done

Configuring the Program

The examples read their settings from the following environment variables, and treat an empty value as unset; each program allows 30 seconds for a reply:

Variable Default Description
MCP_URL http://localhost:8080 The base URL of the MCP Server.
MCP_TOKEN None; it must be set An API token for the server.

The post helper sends any JSON-RPC message with a given token, which lets the program send a wrong token later, and the rpc helper sends a request with the configured token and returns its result.

The following tabs show how each program reads its settings, and how it sends a request to the server:

# The or operator treats an empty variable as unset.
BASE_URL = (os.environ.get("MCP_URL") or "http://localhost:8080").rstrip("/")
TOKEN = os.environ.get("MCP_TOKEN") or ""

TIMEOUT_SECONDS = 30


class McpError(Exception):
    """Raised when the MCP Server rejects a request or replies oddly."""


def post(client: httpx.Client, token: str, message: dict) -> httpx.Response:
    """Posts a JSON-RPC message to the MCP endpoint with a token."""
    return client.post(
        "/mcp/v1", json=message,
        headers={"Authorization": f"Bearer {token}"},
    )


def rpc(client: httpx.Client, method: str, params: dict) -> dict:
    """Sends a JSON-RPC request and returns its result.

    The server answers a JSON-RPC error with HTTP status 200, so the
    function checks the reply for an error member as well as the HTTP
    status.
    """
    response = post(client, TOKEN, {
        "jsonrpc": "2.0", "id": 1, "method": method, "params": params,
    })
    if response.status_code != 200:
        raise McpError(f"{method} failed with HTTP {response.status_code}: "
                       f"{response.text[:200]}")
    try:
        reply = response.json()
    except ValueError:
        reply = None
    if not isinstance(reply, dict) or not ("result" in reply or "error" in reply):
        raise McpError(f"{method} failed: unexpected response: "
                       f"{response.text[:200]}")
    if "error" in reply:
        raise McpError(f"{method} failed: {reply['error'].get('message')}")
    return reply["result"]
// The || operator, unlike ??, also treats an empty variable as unset.
const baseUrl = (process.env.MCP_URL || "http://localhost:8080").replace(/\/+$/, "");
const token = process.env.MCP_TOKEN || "";

const timeoutMs = 30_000;

// Posts a JSON-RPC message to the MCP endpoint with a token.
async function post(withToken: string, message: object): Promise<Response> {
  return fetch(`${baseUrl}/mcp/v1`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${withToken}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify(message),
    signal: AbortSignal.timeout(timeoutMs),
  });
}

// Sends a JSON-RPC request and returns its result. The server answers a
// JSON-RPC error with HTTP status 200, so the function checks the reply
// for an error member as well as the HTTP status.
async function rpc(method: string, params: object): Promise<any> {
  const response = await post(token, { jsonrpc: "2.0", id: 1, method, params });
  const text = await response.text();
  if (response.status !== 200) {
    throw new Error(`${method} failed with HTTP ${response.status}: ${text.slice(0, 200)}`);
  }
  let reply: any;
  try {
    reply = JSON.parse(text);
  } catch {
    reply = undefined;
  }
  if (typeof reply !== "object" || reply === null || !("result" in reply || "error" in reply)) {
    throw new Error(`${method} failed: unexpected response: ${text.slice(0, 200)}`);
  }
  if ("error" in reply) {
    throw new Error(`${method} failed: ${reply.error?.message}`);
  }
  return reply.result;
}
/** Returns the value of an environment variable, or a fallback. */
static String envOr(String name, String fallback) {
    String value = System.getenv(name);
    return (value != null && !value.isEmpty()) ? value : fallback;
}

static final String BASE_URL =
        envOr("MCP_URL", "http://localhost:8080").replaceAll("/+$", "");
static final String TOKEN = envOr("MCP_TOKEN", "");

static final Duration TIMEOUT = Duration.ofSeconds(30);

/** Thrown when the MCP Server rejects a request or replies oddly. */
static class McpException extends Exception {
    McpException(String message) {
        super(message);
    }
}

// A client and a mapper are safe to share, so the program creates
// only one of each.
static final HttpClient CLIENT = HttpClient.newBuilder()
        .connectTimeout(TIMEOUT)
        .build();
static final ObjectMapper MAPPER = JsonMapper.builder().build();

/** Returns at most the first 200 characters of a reply. */
static String shorten(String text) {
    return text.length() > 200 ? text.substring(0, 200) : text;
}

/**
 * Sends a request and returns the response with its whole body. The
 * request timeout stops applying once the headers arrive, so a server
 * that stalls part of the way through the body would hang send(); this
 * applies the timeout to the whole exchange instead.
 */
static HttpResponse<String> send(HttpRequest request)
        throws IOException, InterruptedException {
    CompletableFuture<HttpResponse<String>> pending =
            CLIENT.sendAsync(request, HttpResponse.BodyHandlers.ofString());
    try {
        return pending.get(TIMEOUT.toMillis(), TimeUnit.MILLISECONDS);
    } catch (TimeoutException e) {
        pending.cancel(true);
        throw new HttpTimeoutException("request timed out");
    } catch (ExecutionException e) {
        if (e.getCause() instanceof IOException cause) {
            throw cause;
        }
        throw new IOException(e.getCause());
    }
}

/** Posts a JSON-RPC message to the MCP endpoint with a token. */
static HttpResponse<String> post(String token, Map<String, Object> message)
        throws IOException, InterruptedException {
    HttpRequest request = HttpRequest.newBuilder(URI.create(BASE_URL + "/mcp/v1"))
            .timeout(TIMEOUT)
            .header("Authorization", "Bearer " + token)
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(
                    MAPPER.writeValueAsString(message)))
            .build();
    return send(request);
}

/** Parses a reply as a JSON object, or returns null if it is not one. */
static JsonNode parseObject(String text) {
    try {
        JsonNode node = MAPPER.readTree(text);
        return node.isObject() ? node : null;
    } catch (JacksonException e) {
        return null;
    }
}

/**
 * Sends a JSON-RPC request and returns its result. The server answers a
 * JSON-RPC error with HTTP status 200, so the method checks the reply
 * for an error member as well as the HTTP status.
 */
static JsonNode rpc(String method, Map<String, Object> params)
        throws IOException, InterruptedException, McpException {
    HttpResponse<String> response = post(TOKEN, Map.of(
            "jsonrpc", "2.0", "id", 1, "method", method, "params", params));
    if (response.statusCode() != 200) {
        throw new McpException(method + " failed with HTTP "
                + response.statusCode() + ": " + shorten(response.body()));
    }
    JsonNode reply = parseObject(response.body());
    if (reply == null || !(reply.has("result") || reply.has("error"))) {
        throw new McpException(method + " failed: unexpected response: "
                + shorten(response.body()));
    }
    if (reply.has("error")) {
        throw new McpException(method + " failed: "
                + reply.path("error").path("message").asString());
    }
    return reply.get("result");
}
// Returns the value of an environment variable, or a fallback if the
// variable is unset or empty.
static string EnvOr(string name, string fallback) =>
    Environment.GetEnvironmentVariable(name) is { Length: > 0 } value
        ? value
        : fallback;

static string BaseUrl() => EnvOr("MCP_URL", "http://localhost:8080").TrimEnd('/');
static string Token() => EnvOr("MCP_TOKEN", "");

const int TimeoutSeconds = 30;

// Returns at most the first 200 characters of a reply.
static string Shorten(string text) =>
    text.Length > 200 ? text[..200] : text;

// Parses a reply as a JSON object, or returns null if it is not one.
static JsonObject? ParseObject(string text)
{
    try
    {
        return JsonNode.Parse(text) as JsonObject;
    }
    catch (JsonException)
    {
        return null;
    }
}

// Posts a JSON-RPC message to the MCP endpoint with a token, and returns
// the HTTP status and the body of the reply.
static async Task<(HttpStatusCode Status, string Body)> PostAsync(
    HttpClient client, string token, JsonObject message)
{
    using var request = new HttpRequestMessage(HttpMethod.Post, BaseUrl() + "/mcp/v1")
    {
        Content = new StringContent(
            message.ToJsonString(), Encoding.UTF8, "application/json"),
    };
    request.Headers.Add("Authorization", $"Bearer {token}");
    using var response = await client.SendAsync(request);
    return (response.StatusCode, await response.Content.ReadAsStringAsync());
}

// Sends a JSON-RPC request and returns its result. The server answers a
// JSON-RPC error with HTTP status 200, so the function checks the reply for
// an error member as well as the HTTP status.
static async Task<JsonNode> RpcAsync(HttpClient client, string method, JsonObject parameters)
{
    var (status, body) = await PostAsync(client, Token(), new JsonObject
    {
        ["jsonrpc"] = "2.0",
        ["id"] = 1,
        ["method"] = method,
        ["params"] = parameters,
    });
    if (status != HttpStatusCode.OK)
    {
        throw new McpException(
            $"{method} failed with HTTP {(int)status}: {Shorten(body)}");
    }
    var reply = ParseObject(body);
    if (reply is null || !(reply.ContainsKey("result") || reply.ContainsKey("error")))
    {
        throw new McpException($"{method} failed: unexpected response: {Shorten(body)}");
    }
    if (reply.ContainsKey("error"))
    {
        throw new McpException($"{method} failed: {reply["error"]?["message"]}");
    }
    return reply["result"] ?? throw new McpException($"{method} failed: no result");
}
// envOr returns the value of an environment variable, or a fallback if
// the variable is unset or empty.
func envOr(name, fallback string) string {
    if value := os.Getenv(name); value != "" {
        return value
    }
    return fallback
}

var (
    baseURL = strings.TrimRight(envOr("MCP_URL", "http://localhost:8080"), "/")
    token   = os.Getenv("MCP_TOKEN")
)

const timeout = 30 * time.Second

// shorten returns at most the first 200 bytes of a reply.
func shorten(body []byte) string {
    if len(body) > 200 {
        body = body[:200]
    }
    return string(body)
}

// post sends a JSON-RPC message to the MCP endpoint with a token, and
// returns the HTTP status and the body of the reply.
func post(client *http.Client, withToken string, message any) (int, []byte, error) {
    payload, err := json.Marshal(message)
    if err != nil {
        return 0, nil, err
    }
    request, err := http.NewRequest(http.MethodPost, baseURL+"/mcp/v1", bytes.NewReader(payload))
    if err != nil {
        return 0, nil, err
    }
    request.Header.Set("Authorization", "Bearer "+withToken)
    request.Header.Set("Content-Type", "application/json")

    response, err := client.Do(request)
    if err != nil {
        return 0, nil, err
    }
    defer response.Body.Close()
    body, err := io.ReadAll(response.Body)
    return response.StatusCode, body, err
}

// rpc sends a JSON-RPC request and decodes its result into out. The
// server answers a JSON-RPC error with HTTP status 200, so the function
// checks the reply for an error member as well as the HTTP status.
func rpc(client *http.Client, method string, params any, out any) error {
    status, body, err := post(client, token, map[string]any{
        "jsonrpc": "2.0", "id": 1, "method": method, "params": params,
    })
    if err != nil {
        return err
    }
    if status != http.StatusOK {
        return fmt.Errorf("%s failed with HTTP %d: %s", method, status, shorten(body))
    }
    var reply struct {
        Result json.RawMessage `json:"result"`
        Error  *struct {
            Message string `json:"message"`
        } `json:"error"`
    }
    if err := json.Unmarshal(body, &reply); err != nil || (reply.Result == nil && reply.Error == nil) {
        return fmt.Errorf("%s failed: unexpected response: %s", method, shorten(body))
    }
    if reply.Error != nil {
        return fmt.Errorf("%s failed: %s", method, reply.Error.Message)
    }
    if err := json.Unmarshal(reply.Result, out); err != nil {
        return fmt.Errorf("%s failed: unexpected result: %s", method, shorten(reply.Result))
    }
    return nil
}
/// Returns the value of an environment variable, or a fallback if the
/// variable is unset or empty.
fn env_or(name: &str, fallback: &str) -> String {
    env::var(name)
        .ok()
        .filter(|value| !value.is_empty())
        .unwrap_or_else(|| fallback.to_string())
}

fn base_url() -> String {
    env_or("MCP_URL", "http://localhost:8080")
        .trim_end_matches('/')
        .to_string()
}

fn token() -> String {
    env_or("MCP_TOKEN", "")
}

const TIMEOUT: Duration = Duration::from_secs(30);

/// The errors that the functions below return; a string converts into one
/// with the ? operator or into().
type BoxError = Box<dyn Error>;

/// Returns at most the first 200 characters of a reply.
fn shorten(text: &str) -> String {
    text.chars().take(200).collect()
}

/// Posts a JSON-RPC message to the MCP endpoint with a token, and returns
/// the HTTP status and the body of the reply.
fn post(client: &Client, with_token: &str, message: &Value) -> Result<(u16, String), BoxError> {
    let response = client
        .post(format!("{}/mcp/v1", base_url()))
        .bearer_auth(with_token)
        .json(message)
        .send()?;
    let status = response.status().as_u16();
    Ok((status, response.text()?))
}

/// Sends a JSON-RPC request and returns its result. The server answers a
/// JSON-RPC error with HTTP status 200, so the function checks the reply
/// for an error member as well as the HTTP status.
fn rpc(client: &Client, method: &str, params: Value) -> Result<Value, BoxError> {
    let message = json!({"jsonrpc": "2.0", "id": 1, "method": method, "params": params});
    let (status, body) = post(client, &token(), &message)?;
    if status != 200 {
        return Err(format!("{method} failed with HTTP {status}: {}", shorten(&body)).into());
    }
    let reply = match serde_json::from_str::<Value>(&body) {
        Ok(reply) if reply.get("result").is_some() || reply.get("error").is_some() => reply,
        _ => return Err(format!("{method} failed: unexpected response: {}", shorten(&body)).into()),
    };
    if let Some(error) = reply.get("error") {
        let message = error["message"].as_str().unwrap_or("unknown error");
        return Err(format!("{method} failed: {message}").into());
    }
    Ok(reply["result"].clone())
}
# Returns the value of an environment variable, or a fallback if the
# variable is unset or empty.
def env_or(name, fallback)
  value = ENV[name]
  value.nil? || value.empty? ? fallback : value
end

BASE_URL = env_or("MCP_URL", "http://localhost:8080").sub(%r{/+\z}, "")
TOKEN = env_or("MCP_TOKEN", "")

TIMEOUT_SECONDS = 30

# Raised when the MCP Server rejects a request or replies oddly.
class McpError < StandardError; end

# Parses a reply as a JSON object, or returns nil if it is not one.
def parse_object(text)
  body = JSON.parse(text.to_s)
  body.is_a?(Hash) ? body : nil
rescue JSON::ParserError
  nil
end

# Posts a JSON-RPC message to the MCP endpoint with a token, and returns
# the response.
def post(http, token, message)
  request = Net::HTTP::Post.new(URI("#{BASE_URL}/mcp/v1"))
  request["Authorization"] = "Bearer #{token}"
  request["Content-Type"] = "application/json"
  request.body = JSON.generate(message)
  http.request(request)
end

# Sends a JSON-RPC request and returns its result. The server answers a
# JSON-RPC error with HTTP status 200, so the method checks the reply for
# an error member as well as the HTTP status.
def rpc(http, method, params)
  response = post(http, TOKEN, { jsonrpc: "2.0", id: 1, method: method, params: params })
  unless response.code == "200"
    raise McpError, "#{method} failed with HTTP #{response.code}: #{response.body.to_s[0, 200]}"
  end

  reply = parse_object(response.body)
  unless reply && (reply.key?("result") || reply.key?("error"))
    raise McpError, "#{method} failed: unexpected response: #{response.body.to_s[0, 200]}"
  end
  raise McpError, "#{method} failed: #{reply["error"]["message"]}" if reply.key?("error")

  reply["result"]
end
# Read the settings from the environment. The :- and :? forms, unlike - and
# ?, treat an empty variable as unset.
MCP_URL="${MCP_URL:-http://localhost:8080}"
MCP_URL="${MCP_URL%/}"
: "${MCP_TOKEN:?MCP_TOKEN must be set}"

TIMEOUT_SECONDS=30

# Posts a JSON-RPC message to the MCP endpoint with a token, and prints the
# reply followed by the HTTP status on a line of its own: post TOKEN BODY.
# The token goes to curl in a header file that bash supplies through a
# pipe, so that it never appears in the arguments of the curl process, which
# other users can read in the process list.
post() {
    curl --silent --show-error \
        --connect-timeout 10 --max-time "$TIMEOUT_SECONDS" \
        --header @<(printf 'Authorization: Bearer %s\n' "$1") \
        --header 'Content-Type: application/json' \
        --write-out '\n%{http_code}' --data-binary "$2" "$MCP_URL/mcp/v1"
}

# Sends a JSON-RPC request and prints its result: rpc METHOD PARAMS_JSON.
# The server answers a JSON-RPC error with HTTP status 200, so the function
# checks the reply for an error member as well as the HTTP status.
rpc() {
    local method="$1" request reply status body
    request=$(jq -nc --arg method "$method" --argjson params "$2" \
        '{jsonrpc: "2.0", id: 1, method: $method, params: $params}')
    reply=$(post "$MCP_TOKEN" "$request") || return 1
    status="${reply##*$'\n'}"
    body="${reply%$'\n'*}"
    if [[ "$status" != 200 ]]; then
        echo "$method failed with HTTP $status: ${body:0:200}" >&2
        return 1
    fi
    if ! jq -e 'has("result") or has("error")' >/dev/null 2>&1 <<<"$body"; then
        echo "$method failed: unexpected response: ${body:0:200}" >&2
        return 1
    fi
    if jq -e 'has("error")' >/dev/null <<<"$body"; then
        echo "$method failed: $(jq -r '.error.message' <<<"$body")" >&2
        return 1
    fi
    jq -c '.result' <<<"$body"
}

Checking That the Server Is Running

The /health endpoint takes a GET request without a token, and replies with a JSON object whose status is ok whilst the server runs. Each program stops if the server does not answer, because the other steps cannot succeed without it. The following tabs show the health check:

def check_health(client: httpx.Client) -> None:
    """Checks that the server is running. The /health endpoint needs no
    token."""
    response = client.get("/health")
    if response.status_code != 200:
        raise McpError(f"Health check failed with HTTP "
                       f"{response.status_code}: {response.text[:200]}")
    try:
        status = response.json().get("status")
    except (ValueError, AttributeError):
        status = None
    if status != "ok":
        raise McpError(f"Unexpected response: {response.text[:200]}")
    print("Health: ok")
// Checks that the server is running. The /health endpoint needs no token.
async function checkHealth(): Promise<void> {
  const response = await fetch(`${baseUrl}/health`, {
    signal: AbortSignal.timeout(timeoutMs),
  });
  const text = await response.text();
  if (response.status !== 200) {
    throw new Error(`Health check failed with HTTP ${response.status}: ${text.slice(0, 200)}`);
  }
  let status: unknown;
  try {
    status = JSON.parse(text)?.status;
  } catch {
    status = undefined;
  }
  if (status !== "ok") {
    throw new Error(`Unexpected response: ${text.slice(0, 200)}`);
  }
  console.log("Health: ok");
}
/** Checks that the server is running. The /health endpoint needs no token. */
static void checkHealth() throws IOException, InterruptedException, McpException {
    HttpRequest request = HttpRequest.newBuilder(URI.create(BASE_URL + "/health"))
            .timeout(TIMEOUT)
            .GET()
            .build();
    HttpResponse<String> response = send(request);
    if (response.statusCode() != 200) {
        throw new McpException("Health check failed with HTTP "
                + response.statusCode() + ": " + shorten(response.body()));
    }
    JsonNode body = parseObject(response.body());
    if (body == null || !"ok".equals(body.path("status").asString(""))) {
        throw new McpException("Unexpected response: " + shorten(response.body()));
    }
    System.out.println("Health: ok");
}
// Checks that the server is running. The /health endpoint needs no token.
static async Task CheckHealthAsync(HttpClient client)
{
    using var response = await client.GetAsync(BaseUrl() + "/health");
    var body = await response.Content.ReadAsStringAsync();
    if (response.StatusCode != HttpStatusCode.OK)
    {
        throw new McpException(
            $"Health check failed with HTTP {(int)response.StatusCode}: {Shorten(body)}");
    }
    if (ParseObject(body)?["status"] is not JsonValue status ||
        status.ToString() != "ok")
    {
        throw new McpException($"Unexpected response: {Shorten(body)}");
    }
    Console.WriteLine("Health: ok");
}
// checkHealth checks that the server is running. The /health endpoint
// needs no token.
func checkHealth(client *http.Client) error {
    response, err := client.Get(baseURL + "/health")
    if err != nil {
        return err
    }
    defer response.Body.Close()
    body, err := io.ReadAll(response.Body)
    if err != nil {
        return err
    }
    if response.StatusCode != http.StatusOK {
        return fmt.Errorf("health check failed with HTTP %d: %s", response.StatusCode, shorten(body))
    }
    var health struct {
        Status string `json:"status"`
    }
    if err := json.Unmarshal(body, &health); err != nil || health.Status != "ok" {
        return fmt.Errorf("unexpected response: %s", shorten(body))
    }
    fmt.Println("Health: ok")
    return nil
}
/// Checks that the server is running. The /health endpoint needs no token.
fn check_health(client: &Client) -> Result<(), BoxError> {
    let response = client.get(format!("{}/health", base_url())).send()?;
    let status = response.status().as_u16();
    let body = response.text()?;
    if status != 200 {
        return Err(format!("Health check failed with HTTP {status}: {}", shorten(&body)).into());
    }
    let health: Value = serde_json::from_str(&body).unwrap_or(Value::Null);
    if health["status"].as_str() != Some("ok") {
        return Err(format!("Unexpected response: {}", shorten(&body)).into());
    }
    println!("Health: ok");
    Ok(())
}
# Checks that the server is running. The /health endpoint needs no token.
def check_health(http)
  response = http.request(Net::HTTP::Get.new(URI("#{BASE_URL}/health")))
  unless response.code == "200"
    raise McpError, "Health check failed with HTTP #{response.code}: #{response.body.to_s[0, 200]}"
  end
  unless parse_object(response.body)&.fetch("status", nil) == "ok"
    raise McpError, "Unexpected response: #{response.body.to_s[0, 200]}"
  end

  puts "Health: ok"
end
# Checks that the server is running. The /health endpoint needs no token.
check_health() {
    local body
    body=$(curl --silent --show-error --fail-with-body \
        --connect-timeout 10 --max-time "$TIMEOUT_SECONDS" \
        "$MCP_URL/health") || {
        echo "Health check failed: $body" >&2
        return 1
    }
    jq -er 'if .status == "ok" then "Health: ok"
            else error("status is not ok") end' <<<"$body" 2>/dev/null || {
        echo "Unexpected response: ${body:0:200}" >&2
        return 1
    }
}

Introducing the Client

An MCP client starts a session with the initialize method, which gives the protocol revision that the client wants, the client's capabilities, and its name and version. The server replies with its own name and version and the revision that it has chosen; version 1.1.0 supports the revisions 2024-11-05, 2025-03-26, 2025-06-18 and 2025-11-25, and answers a revision that it does not know with one that it does, so a client must check the answer. The client then sends the notifications/initialized notification, which has no id, and the server replies with HTTP 202 and no body. The following tabs show the handshake:

def initialize(client: httpx.Client) -> None:
    """Introduces the client to the server, and prints the server's name and
    version and the protocol revision that the server chose.

    The server accepts 2024-11-05, 2025-03-26, 2025-06-18 and 2025-11-25,
    and answers a revision it does not know with one that it does, so the
    client checks the answer. The notifications/initialized message then
    completes the handshake; it is a notification, with no id, so the
    server replies with HTTP 202 and no body.
    """
    result = rpc(client, "initialize", {
        "protocolVersion": "2025-06-18",
        "capabilities": {},
        "clientInfo": {"name": "cookbook", "version": "1.0.0"},
    })
    info = result["serverInfo"]
    print(f"Server: {info['name']} {info['version']}")
    protocol = result.get("protocolVersion")
    if protocol != "2025-06-18":
        raise McpError(f"The server chose protocol version {protocol}")
    print(f"Protocol version: {protocol}")

    response = post(client, TOKEN, {
        "jsonrpc": "2.0", "method": "notifications/initialized",
    })
    if response.status_code != 202:
        raise McpError(f"notifications/initialized failed with HTTP "
                       f"{response.status_code}: {response.text[:200]}")
// Introduces the client to the server, and prints the server's name and
// version and the protocol revision that the server chose. The server
// accepts 2024-11-05, 2025-03-26, 2025-06-18 and 2025-11-25, and answers a
// revision it does not know with one that it does, so the client checks
// the answer. The notifications/initialized message then completes the
// handshake; it is a notification, with no id, so the server replies with
// HTTP 202 and no body.
async function initialize(): Promise<void> {
  const result = await rpc("initialize", {
    protocolVersion: "2025-06-18",
    capabilities: {},
    clientInfo: { name: "cookbook", version: "1.0.0" },
  });
  const info = result?.serverInfo;
  if (typeof info?.name !== "string" || typeof info?.version !== "string") {
    throw new Error(`Unexpected result: ${JSON.stringify(result)}`);
  }
  console.log(`Server: ${info.name} ${info.version}`);
  const protocol = result.protocolVersion;
  if (protocol !== "2025-06-18") {
    throw new Error(`The server chose protocol version ${protocol}`);
  }
  console.log(`Protocol version: ${protocol}`);

  const response = await post(token, {
    jsonrpc: "2.0",
    method: "notifications/initialized",
  });
  const text = await response.text();
  if (response.status !== 202) {
    throw new Error(
      `notifications/initialized failed with HTTP ${response.status}: ${text.slice(0, 200)}`,
    );
  }
}
/**
 * Introduces the client to the server, and prints the server's name and
 * version and the protocol revision that the server chose. The server
 * accepts 2024-11-05, 2025-03-26, 2025-06-18 and 2025-11-25, and answers
 * a revision it does not know with one that it does, so the client
 * checks the answer. The notifications/initialized message then
 * completes the handshake; it is a notification, with no id, so the
 * server replies with HTTP 202 and no body.
 */
static void initialize() throws IOException, InterruptedException, McpException {
    JsonNode result = rpc("initialize", Map.of(
            "protocolVersion", "2025-06-18",
            "capabilities", Map.of(),
            "clientInfo", Map.of("name", "cookbook", "version", "1.0.0")));
    JsonNode info = result.path("serverInfo");
    if (!info.path("name").isString() || !info.path("version").isString()) {
        throw new McpException("Unexpected result: " + result);
    }
    System.out.println("Server: " + info.path("name").stringValue()
            + " " + info.path("version").stringValue());
    String protocol = result.path("protocolVersion").asString("");
    if (!protocol.equals("2025-06-18")) {
        throw new McpException("The server chose protocol version " + protocol);
    }
    System.out.println("Protocol version: " + protocol);

    HttpResponse<String> response = post(TOKEN, Map.of(
            "jsonrpc", "2.0", "method", "notifications/initialized"));
    if (response.statusCode() != 202) {
        throw new McpException("notifications/initialized failed with HTTP "
                + response.statusCode() + ": " + shorten(response.body()));
    }
}
// Introduces the client to the server, and prints the server's name and
// version and the protocol revision that the server chose. The server
// accepts 2024-11-05, 2025-03-26, 2025-06-18 and 2025-11-25, and answers a
// revision it does not know with one that it does, so the client checks
// the answer. The notifications/initialized message then completes the
// handshake; it is a notification, with no id, so the server replies with
// HTTP 202 and no body.
static async Task InitializeAsync(HttpClient client)
{
    var result = await RpcAsync(client, "initialize", new JsonObject
    {
        ["protocolVersion"] = "2025-06-18",
        ["capabilities"] = new JsonObject(),
        ["clientInfo"] = new JsonObject { ["name"] = "cookbook", ["version"] = "1.0.0" },
    });
    if (result["serverInfo"]?["name"] is not JsonValue name ||
        result["serverInfo"]?["version"] is not JsonValue version)
    {
        throw new McpException($"Unexpected result: {result.ToJsonString()}");
    }
    Console.WriteLine($"Server: {name} {version}");
    var protocol = result["protocolVersion"]?.ToString();
    if (protocol != "2025-06-18")
    {
        throw new McpException($"The server chose protocol version {protocol}");
    }
    Console.WriteLine($"Protocol version: {protocol}");

    var (status, body) = await PostAsync(client, Token(), new JsonObject
    {
        ["jsonrpc"] = "2.0",
        ["method"] = "notifications/initialized",
    });
    if (status != HttpStatusCode.Accepted)
    {
        throw new McpException(
            $"notifications/initialized failed with HTTP {(int)status}: {Shorten(body)}");
    }
}
// initialize introduces the client to the server, and prints the server's
// name and version and the protocol revision that the server chose. The
// server accepts 2024-11-05, 2025-03-26, 2025-06-18 and 2025-11-25, and
// answers a revision it does not know with one that it does, so the client
// checks the answer. The notifications/initialized message then completes
// the handshake; it is a notification, with no id, so the server replies
// with HTTP 202 and no body.
func initialize(client *http.Client) error {
    var result struct {
        ProtocolVersion string `json:"protocolVersion"`
        ServerInfo      *struct {
            Name    string `json:"name"`
            Version string `json:"version"`
        } `json:"serverInfo"`
    }
    err := rpc(client, "initialize", map[string]any{
        "protocolVersion": "2025-06-18",
        "capabilities":    map[string]any{},
        "clientInfo":      map[string]string{"name": "cookbook", "version": "1.0.0"},
    }, &result)
    if err != nil {
        return err
    }
    if result.ServerInfo == nil {
        return errors.New("initialize failed: the result has no serverInfo")
    }
    fmt.Printf("Server: %s %s\n", result.ServerInfo.Name, result.ServerInfo.Version)
    if result.ProtocolVersion != "2025-06-18" {
        return fmt.Errorf("the server chose protocol version %s", result.ProtocolVersion)
    }
    fmt.Printf("Protocol version: %s\n", result.ProtocolVersion)

    status, body, err := post(client, token, map[string]any{
        "jsonrpc": "2.0", "method": "notifications/initialized",
    })
    if err != nil {
        return err
    }
    if status != http.StatusAccepted {
        return fmt.Errorf("notifications/initialized failed with HTTP %d: %s", status, shorten(body))
    }
    return nil
}
/// Introduces the client to the server, and prints the server's name and
/// version and the protocol revision that the server chose. The server
/// accepts 2024-11-05, 2025-03-26, 2025-06-18 and 2025-11-25, and answers a
/// revision it does not know with one that it does, so the client checks
/// the answer. The notifications/initialized message then completes the
/// handshake; it is a notification, with no id, so the server replies with
/// HTTP 202 and no body.
fn initialize(client: &Client) -> Result<(), BoxError> {
    let result = rpc(
        client,
        "initialize",
        json!({
            "protocolVersion": "2025-06-18",
            "capabilities": {},
            "clientInfo": {"name": "cookbook", "version": "1.0.0"},
        }),
    )?;
    let info = &result["serverInfo"];
    let (Some(name), Some(version)) = (info["name"].as_str(), info["version"].as_str()) else {
        return Err(format!("Unexpected result: {result}").into());
    };
    println!("Server: {name} {version}");
    let protocol = result["protocolVersion"].as_str().unwrap_or("");
    if protocol != "2025-06-18" {
        return Err(format!("The server chose protocol version {protocol}").into());
    }
    println!("Protocol version: {protocol}");

    let message = json!({"jsonrpc": "2.0", "method": "notifications/initialized"});
    let (status, body) = post(client, &token(), &message)?;
    if status != 202 {
        return Err(format!(
            "notifications/initialized failed with HTTP {status}: {}",
            shorten(&body)
        )
        .into());
    }
    Ok(())
}
# Introduces the client to the server, and prints the server's name and
# version and the protocol revision that the server chose. The server
# accepts 2024-11-05, 2025-03-26, 2025-06-18 and 2025-11-25, and answers a
# revision it does not know with one that it does, so the client checks
# the answer. The notifications/initialized message then completes the
# handshake; it is a notification, with no id, so the server replies with
# HTTP 202 and no body.
def initialize_session(http)
  result = rpc(http, "initialize", {
                 protocolVersion: "2025-06-18",
                 capabilities: {},
                 clientInfo: { name: "cookbook", version: "1.0.0" }
               })
  info = result.is_a?(Hash) ? result["serverInfo"] : nil
  unless info.is_a?(Hash) && info["name"].is_a?(String) && info["version"].is_a?(String)
    raise McpError, "Unexpected result: #{result}"
  end

  puts "Server: #{info["name"]} #{info["version"]}"
  protocol = result["protocolVersion"]
  raise McpError, "The server chose protocol version #{protocol}" unless protocol == "2025-06-18"

  puts "Protocol version: #{protocol}"

  response = post(http, TOKEN, { jsonrpc: "2.0", method: "notifications/initialized" })
  return if response.code == "202"

  raise McpError, "notifications/initialized failed with HTTP #{response.code}: " \
                  "#{response.body.to_s[0, 200]}"
end
# Introduces the client to the server, and prints the server's name and
# version and the protocol revision that the server chose. The server
# accepts 2024-11-05, 2025-03-26, 2025-06-18 and 2025-11-25, and answers a
# revision it does not know with one that it does, so the client checks
# the answer. The notifications/initialized message then completes the
# handshake; it is a notification, with no id, so the server replies with
# HTTP 202 and no body.
initialize() {
    local result protocol reply
    result=$(rpc initialize '{
        "protocolVersion": "2025-06-18",
        "capabilities": {},
        "clientInfo": {"name": "cookbook", "version": "1.0.0"}
    }') || return 1
    jq -r '"Server: \(.serverInfo.name) \(.serverInfo.version)"' <<<"$result"
    protocol=$(jq -r '.protocolVersion' <<<"$result")
    if [[ "$protocol" != "2025-06-18" ]]; then
        echo "The server chose protocol version $protocol" >&2
        return 1
    fi
    echo "Protocol version: $protocol"

    reply=$(post "$MCP_TOKEN" \
        '{"jsonrpc": "2.0", "method": "notifications/initialized"}') || return 1
    if [[ "${reply##*$'\n'}" != 202 ]]; then
        echo "notifications/initialized failed: ${reply:0:200}" >&2
        return 1
    fi
}

Sending a Wrong Token

The server rejects a token that it does not know with HTTP 401 and a JSON object with an error member, which is not a JSON-RPC reply. Each program sends a made-up token, and stops if the server does not reject it. The following tabs show the request:

def wrong_token(client: httpx.Client) -> None:
    """Sends a request with a token that the server does not know, and
    prints the HTTP status and the server's message, which is a JSON object
    with an error member rather than a JSON-RPC reply."""
    response = post(client, "not-a-real-token", {
        "jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {},
    })
    if response.status_code != 401:
        raise McpError(f"Expected HTTP 401 for a wrong token, but got "
                       f"{response.status_code}")
    try:
        message = response.json().get("error") or "no message"
    except (ValueError, AttributeError):
        message = "no message"
    print(f"Wrong token: HTTP {response.status_code}, {message}")
// Sends a request with a token that the server does not know, and prints
// the HTTP status and the server's message, which is a JSON object with an
// error member rather than a JSON-RPC reply.
async function wrongToken(): Promise<void> {
  const response = await post("not-a-real-token", {
    jsonrpc: "2.0",
    id: 1,
    method: "tools/list",
    params: {},
  });
  const text = await response.text();
  if (response.status !== 401) {
    throw new Error(`Expected HTTP 401 for a wrong token, but got ${response.status}`);
  }
  let message = "no message";
  try {
    message = JSON.parse(text)?.error || message;
  } catch {
    // The body is not JSON; keep the default message.
  }
  console.log(`Wrong token: HTTP ${response.status}, ${message}`);
}
/**
 * Sends a request with a token that the server does not know, and
 * prints the HTTP status and the server's message, which is a JSON
 * object with an error member rather than a JSON-RPC reply.
 */
static void wrongToken() throws IOException, InterruptedException, McpException {
    HttpResponse<String> response = post("not-a-real-token", Map.of(
            "jsonrpc", "2.0", "id", 1, "method", "tools/list",
            "params", Map.of()));
    if (response.statusCode() != 401) {
        throw new McpException("Expected HTTP 401 for a wrong token, but got "
                + response.statusCode());
    }
    JsonNode body = parseObject(response.body());
    String message = body == null ? "" : body.path("error").asString("");
    if (message.isEmpty()) {
        message = "no message";
    }
    System.out.println("Wrong token: HTTP " + response.statusCode() + ", " + message);
}
// Sends a request with a token that the server does not know, and prints
// the HTTP status and the server's message, which is a JSON object with an
// error member rather than a JSON-RPC reply.
static async Task WrongTokenAsync(HttpClient client)
{
    var (status, body) = await PostAsync(client, "not-a-real-token", new JsonObject
    {
        ["jsonrpc"] = "2.0",
        ["id"] = 1,
        ["method"] = "tools/list",
        ["params"] = new JsonObject(),
    });
    if (status != HttpStatusCode.Unauthorized)
    {
        throw new McpException(
            $"Expected HTTP 401 for a wrong token, but got {(int)status}");
    }
    var message = ParseObject(body)?["error"]?.ToString();
    if (string.IsNullOrEmpty(message))
    {
        message = "no message";
    }
    Console.WriteLine($"Wrong token: HTTP {(int)status}, {message}");
}
// wrongToken sends a request with a token that the server does not know,
// and prints the HTTP status and the server's message, which is a JSON
// object with an error member rather than a JSON-RPC reply.
func wrongToken(client *http.Client) error {
    status, body, err := post(client, "not-a-real-token", map[string]any{
        "jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": map[string]any{},
    })
    if err != nil {
        return err
    }
    if status != http.StatusUnauthorized {
        return fmt.Errorf("expected HTTP 401 for a wrong token, but got %d", status)
    }
    var reply struct {
        Error string `json:"error"`
    }
    message := "no message"
    if json.Unmarshal(body, &reply) == nil && reply.Error != "" {
        message = reply.Error
    }
    fmt.Printf("Wrong token: HTTP %d, %s\n", status, message)
    return nil
}
/// Sends a request with a token that the server does not know, and prints
/// the HTTP status and the server's message, which is a JSON object with
/// an error member rather than a JSON-RPC reply.
fn wrong_token(client: &Client) -> Result<(), BoxError> {
    let message = json!({"jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {}});
    let (status, body) = post(client, "not-a-real-token", &message)?;
    if status != 401 {
        return Err(format!("Expected HTTP 401 for a wrong token, but got {status}").into());
    }
    let reply: Value = serde_json::from_str(&body).unwrap_or(Value::Null);
    let message = match reply["error"].as_str() {
        Some(text) if !text.is_empty() => text,
        _ => "no message",
    };
    println!("Wrong token: HTTP {status}, {message}");
    Ok(())
}
# Sends a request with a token that the server does not know, and prints
# the HTTP status and the server's message, which is a JSON object with an
# error member rather than a JSON-RPC reply.
def wrong_token(http)
  response = post(http, "not-a-real-token",
                  { jsonrpc: "2.0", id: 1, method: "tools/list", params: {} })
  unless response.code == "401"
    raise McpError, "Expected HTTP 401 for a wrong token, but got #{response.code}"
  end

  message = parse_object(response.body)&.fetch("error", nil)
  message = "no message" if message.nil? || message.to_s.empty?
  puts "Wrong token: HTTP #{response.code}, #{message}"
end
# Sends a request with a token that the server does not know, and prints
# the HTTP status and the server's message, which is a JSON object with an
# error member rather than a JSON-RPC reply.
wrong_token() {
    local reply status body
    reply=$(post "not-a-real-token" \
        '{"jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {}}') \
        || return 1
    status="${reply##*$'\n'}"
    body="${reply%$'\n'*}"
    if [[ "$status" != 401 ]]; then
        echo "Expected HTTP 401 for a wrong token, but got $status" >&2
        return 1
    fi
    echo "Wrong token: HTTP $status, $(jq -r '.error // "no message"' <<<"$body")"
}

Handling Errors

The main part of each program checks that a token is set, runs the three steps, reports any failure on stderr, and exits with a non-zero status, so that a script can act on the result. The following tabs show the main part of each program:

def main() -> int:
    if not TOKEN:
        print("MCP_TOKEN must be set", file=sys.stderr)
        return 1
    try:
        with httpx.Client(base_url=BASE_URL, timeout=TIMEOUT_SECONDS) as client:
            check_health(client)
            initialize(client)
            wrong_token(client)
    # httpx.HTTPError is the base class for failures to reach the server,
    # such as a refused connection or a timeout. KeyError, IndexError,
    # TypeError, AttributeError and ValueError cover a reply that lacks the
    # members the program reads, or holds one of the wrong type or form.
    except (httpx.HTTPError, McpError, KeyError, IndexError, TypeError,
            AttributeError, ValueError) as e:
        print(f"MCP Server error: {e}", file=sys.stderr)
        return 1
    return 0


if __name__ == "__main__":
    sys.exit(main())
// Returns a readable description of an error. fetch reports a connection
// failure as a TypeError with the cause (often an AggregateError, whose
// own message is empty) attached.
function describe(err: unknown): string {
  if (err instanceof AggregateError) {
    return err.errors.map(describe).join("; ");
  }
  if (err instanceof Error) {
    return err.cause ? `${err.message}: ${describe(err.cause)}` : err.message;
  }
  return String(err);
}

async function main(): Promise<number> {
  if (!token) {
    console.error("MCP_TOKEN must be set");
    return 1;
  }
  try {
    await checkHealth();
    await initialize();
    await wrongToken();
    return 0;
  } catch (err) {
    console.error(`MCP Server error: ${describe(err)}`);
    return 1;
  }
}

process.exitCode = await main();
public static void main(String[] args) {
    if (TOKEN.isEmpty()) {
        System.err.println("MCP_TOKEN must be set");
        System.exit(1);
    }
    try {
        checkHealth();
        initialize();
        wrongToken();
    // IOException covers failures to reach the server, such as a
    // refused connection or a timeout. The message of a refused
    // connection can be empty, so print the exception itself, which
    // includes its type.
    } catch (IOException | InterruptedException e) {
        System.err.println("MCP Server error: " + e);
        System.exit(1);
    } catch (McpException e) {
        System.err.println("MCP Server error: " + e.getMessage());
        System.exit(1);
    }
}
if (Token().Length == 0)
{
    Console.Error.WriteLine("MCP_TOKEN must be set");
    return 1;
}
try
{
    // using disposes of the client, and closes its connections, when the
    // block exits, whether or not an error occurred.
    using var client = new HttpClient
    {
        Timeout = TimeSpan.FromSeconds(TimeoutSeconds),
    };
    await CheckHealthAsync(client);
    await InitializeAsync(client);
    await WrongTokenAsync(client);
}
// HttpRequestException covers failures to reach the server, such as a
// refused connection, and TaskCanceledException covers a timeout.
catch (Exception e) when (
    e is HttpRequestException or TaskCanceledException or McpException)
{
    Console.Error.WriteLine($"MCP Server error: {e.Message}");
    return 1;
}
return 0;

// C# requires type declarations to follow the top-level statements, so
// the exception that the program throws for an error reply is declared
// here, at the end.
class McpException(string message) : Exception(message);
// run makes the three checks and returns any error to the caller.
func run() error {
    if token == "" {
        return errors.New("MCP_TOKEN must be set")
    }
    client := &http.Client{Timeout: timeout}
    if err := checkHealth(client); err != nil {
        return err
    }
    if err := initialize(client); err != nil {
        return err
    }
    return wrongToken(client)
}

// main reports any error returned by run() and exits with a non-zero
// status.
func main() {
    if err := run(); err != nil {
        fmt.Fprintf(os.Stderr, "MCP Server error: %v\n", err)
        os.Exit(1)
    }
}
/// Makes the three checks. The ? operator returns any error to the
/// caller.
fn run() -> Result<(), BoxError> {
    if token().is_empty() {
        return Err("MCP_TOKEN must be set".into());
    }
    let client = Client::builder().timeout(TIMEOUT).build()?;
    check_health(&client)?;
    initialize(&client)?;
    wrong_token(&client)
}

/// Reports any error returned by run() and exits with a failure status.
/// The top-level error is often generic ("error sending request"), so the
/// chain of underlying causes is printed too.
fn main() -> ExitCode {
    match run() {
        Ok(()) => ExitCode::SUCCESS,
        Err(e) => {
            eprintln!("MCP Server error: {e}");
            let mut cause = e.source();
            while let Some(c) = cause {
                eprintln!("  caused by: {c}");
                cause = c.source();
            }
            ExitCode::FAILURE
        }
    }
}
if TOKEN.empty?
  warn "MCP_TOKEN must be set"
  exit 1
end

begin
  uri = URI(BASE_URL)
  # start opens a connection, and closes it when the block exits, whether
  # or not an error occurred.
  Net::HTTP.start(
    uri.host, uri.port,
    use_ssl: uri.scheme == "https",
    open_timeout: TIMEOUT_SECONDS, read_timeout: TIMEOUT_SECONDS
  ) do |http|
    check_health(http)
    initialize_session(http)
    wrong_token(http)
  end
# SystemCallError covers a refused connection, Timeout::Error covers the
# connection and read timeouts, and SocketError covers an unknown host.
rescue McpError, SystemCallError, Timeout::Error, SocketError, IOError => e
  warn "MCP Server error: #{e.message}"
  exit 1
end
main() {
    check_health
    initialize
    wrong_token
}

main

Running the Example

The complete project for each language is available in the cookbook repository. Set MCP_URL and MCP_TOKEN to the values that scripts/start_postgres_mcp.sh prints, or to those of your own server, and then run the command for your language from the corresponding project directory. The following tabs show the commands that build and run each example:

python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
.venv/bin/python main.py
npm install
npm run build
npm start
mvn compile exec:java
dotnet run
go run .
cargo run
bundle install
bundle exec ruby main.rb
bash main.sh

Viewing the Complete Program

The following tabs show the complete program for each language:

"""
Ellie's Postgres Cookbook
Connecting to the pgEdge Postgres MCP Server, using httpx.

These examples are for pgEdge Postgres MCP Server 1.1.0.

Copyright (c) 2026, pgEdge, Inc.
This code is released under the PostgreSQL License.
"""

import os
import sys

import httpx


# The or operator treats an empty variable as unset.
BASE_URL = (os.environ.get("MCP_URL") or "http://localhost:8080").rstrip("/")
TOKEN = os.environ.get("MCP_TOKEN") or ""

TIMEOUT_SECONDS = 30


class McpError(Exception):
    """Raised when the MCP Server rejects a request or replies oddly."""


def post(client: httpx.Client, token: str, message: dict) -> httpx.Response:
    """Posts a JSON-RPC message to the MCP endpoint with a token."""
    return client.post(
        "/mcp/v1", json=message,
        headers={"Authorization": f"Bearer {token}"},
    )


def rpc(client: httpx.Client, method: str, params: dict) -> dict:
    """Sends a JSON-RPC request and returns its result.

    The server answers a JSON-RPC error with HTTP status 200, so the
    function checks the reply for an error member as well as the HTTP
    status.
    """
    response = post(client, TOKEN, {
        "jsonrpc": "2.0", "id": 1, "method": method, "params": params,
    })
    if response.status_code != 200:
        raise McpError(f"{method} failed with HTTP {response.status_code}: "
                       f"{response.text[:200]}")
    try:
        reply = response.json()
    except ValueError:
        reply = None
    if not isinstance(reply, dict) or not ("result" in reply or "error" in reply):
        raise McpError(f"{method} failed: unexpected response: "
                       f"{response.text[:200]}")
    if "error" in reply:
        raise McpError(f"{method} failed: {reply['error'].get('message')}")
    return reply["result"]


def check_health(client: httpx.Client) -> None:
    """Checks that the server is running. The /health endpoint needs no
    token."""
    response = client.get("/health")
    if response.status_code != 200:
        raise McpError(f"Health check failed with HTTP "
                       f"{response.status_code}: {response.text[:200]}")
    try:
        status = response.json().get("status")
    except (ValueError, AttributeError):
        status = None
    if status != "ok":
        raise McpError(f"Unexpected response: {response.text[:200]}")
    print("Health: ok")


def initialize(client: httpx.Client) -> None:
    """Introduces the client to the server, and prints the server's name and
    version and the protocol revision that the server chose.

    The server accepts 2024-11-05, 2025-03-26, 2025-06-18 and 2025-11-25,
    and answers a revision it does not know with one that it does, so the
    client checks the answer. The notifications/initialized message then
    completes the handshake; it is a notification, with no id, so the
    server replies with HTTP 202 and no body.
    """
    result = rpc(client, "initialize", {
        "protocolVersion": "2025-06-18",
        "capabilities": {},
        "clientInfo": {"name": "cookbook", "version": "1.0.0"},
    })
    info = result["serverInfo"]
    print(f"Server: {info['name']} {info['version']}")
    protocol = result.get("protocolVersion")
    if protocol != "2025-06-18":
        raise McpError(f"The server chose protocol version {protocol}")
    print(f"Protocol version: {protocol}")

    response = post(client, TOKEN, {
        "jsonrpc": "2.0", "method": "notifications/initialized",
    })
    if response.status_code != 202:
        raise McpError(f"notifications/initialized failed with HTTP "
                       f"{response.status_code}: {response.text[:200]}")


def wrong_token(client: httpx.Client) -> None:
    """Sends a request with a token that the server does not know, and
    prints the HTTP status and the server's message, which is a JSON object
    with an error member rather than a JSON-RPC reply."""
    response = post(client, "not-a-real-token", {
        "jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {},
    })
    if response.status_code != 401:
        raise McpError(f"Expected HTTP 401 for a wrong token, but got "
                       f"{response.status_code}")
    try:
        message = response.json().get("error") or "no message"
    except (ValueError, AttributeError):
        message = "no message"
    print(f"Wrong token: HTTP {response.status_code}, {message}")


def main() -> int:
    if not TOKEN:
        print("MCP_TOKEN must be set", file=sys.stderr)
        return 1
    try:
        with httpx.Client(base_url=BASE_URL, timeout=TIMEOUT_SECONDS) as client:
            check_health(client)
            initialize(client)
            wrong_token(client)
    # httpx.HTTPError is the base class for failures to reach the server,
    # such as a refused connection or a timeout. KeyError, IndexError,
    # TypeError, AttributeError and ValueError cover a reply that lacks the
    # members the program reads, or holds one of the wrong type or form.
    except (httpx.HTTPError, McpError, KeyError, IndexError, TypeError,
            AttributeError, ValueError) as e:
        print(f"MCP Server error: {e}", file=sys.stderr)
        return 1
    return 0


if __name__ == "__main__":
    sys.exit(main())
/*
 * Ellie's Postgres Cookbook
 * Connecting to the pgEdge Postgres MCP Server, using the built-in fetch.
 *
 * These examples are for pgEdge Postgres MCP Server 1.1.0.
 *
 * Copyright (c) 2026, pgEdge, Inc.
 * This code is released under the PostgreSQL License.
 */

// Node.js provides fetch and AbortSignal as globals, so this program
// imports nothing.

// The || operator, unlike ??, also treats an empty variable as unset.
const baseUrl = (process.env.MCP_URL || "http://localhost:8080").replace(/\/+$/, "");
const token = process.env.MCP_TOKEN || "";

const timeoutMs = 30_000;

// Posts a JSON-RPC message to the MCP endpoint with a token.
async function post(withToken: string, message: object): Promise<Response> {
  return fetch(`${baseUrl}/mcp/v1`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${withToken}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify(message),
    signal: AbortSignal.timeout(timeoutMs),
  });
}

// Sends a JSON-RPC request and returns its result. The server answers a
// JSON-RPC error with HTTP status 200, so the function checks the reply
// for an error member as well as the HTTP status.
async function rpc(method: string, params: object): Promise<any> {
  const response = await post(token, { jsonrpc: "2.0", id: 1, method, params });
  const text = await response.text();
  if (response.status !== 200) {
    throw new Error(`${method} failed with HTTP ${response.status}: ${text.slice(0, 200)}`);
  }
  let reply: any;
  try {
    reply = JSON.parse(text);
  } catch {
    reply = undefined;
  }
  if (typeof reply !== "object" || reply === null || !("result" in reply || "error" in reply)) {
    throw new Error(`${method} failed: unexpected response: ${text.slice(0, 200)}`);
  }
  if ("error" in reply) {
    throw new Error(`${method} failed: ${reply.error?.message}`);
  }
  return reply.result;
}

// Checks that the server is running. The /health endpoint needs no token.
async function checkHealth(): Promise<void> {
  const response = await fetch(`${baseUrl}/health`, {
    signal: AbortSignal.timeout(timeoutMs),
  });
  const text = await response.text();
  if (response.status !== 200) {
    throw new Error(`Health check failed with HTTP ${response.status}: ${text.slice(0, 200)}`);
  }
  let status: unknown;
  try {
    status = JSON.parse(text)?.status;
  } catch {
    status = undefined;
  }
  if (status !== "ok") {
    throw new Error(`Unexpected response: ${text.slice(0, 200)}`);
  }
  console.log("Health: ok");
}

// Introduces the client to the server, and prints the server's name and
// version and the protocol revision that the server chose. The server
// accepts 2024-11-05, 2025-03-26, 2025-06-18 and 2025-11-25, and answers a
// revision it does not know with one that it does, so the client checks
// the answer. The notifications/initialized message then completes the
// handshake; it is a notification, with no id, so the server replies with
// HTTP 202 and no body.
async function initialize(): Promise<void> {
  const result = await rpc("initialize", {
    protocolVersion: "2025-06-18",
    capabilities: {},
    clientInfo: { name: "cookbook", version: "1.0.0" },
  });
  const info = result?.serverInfo;
  if (typeof info?.name !== "string" || typeof info?.version !== "string") {
    throw new Error(`Unexpected result: ${JSON.stringify(result)}`);
  }
  console.log(`Server: ${info.name} ${info.version}`);
  const protocol = result.protocolVersion;
  if (protocol !== "2025-06-18") {
    throw new Error(`The server chose protocol version ${protocol}`);
  }
  console.log(`Protocol version: ${protocol}`);

  const response = await post(token, {
    jsonrpc: "2.0",
    method: "notifications/initialized",
  });
  const text = await response.text();
  if (response.status !== 202) {
    throw new Error(
      `notifications/initialized failed with HTTP ${response.status}: ${text.slice(0, 200)}`,
    );
  }
}

// Sends a request with a token that the server does not know, and prints
// the HTTP status and the server's message, which is a JSON object with an
// error member rather than a JSON-RPC reply.
async function wrongToken(): Promise<void> {
  const response = await post("not-a-real-token", {
    jsonrpc: "2.0",
    id: 1,
    method: "tools/list",
    params: {},
  });
  const text = await response.text();
  if (response.status !== 401) {
    throw new Error(`Expected HTTP 401 for a wrong token, but got ${response.status}`);
  }
  let message = "no message";
  try {
    message = JSON.parse(text)?.error || message;
  } catch {
    // The body is not JSON; keep the default message.
  }
  console.log(`Wrong token: HTTP ${response.status}, ${message}`);
}

// Returns a readable description of an error. fetch reports a connection
// failure as a TypeError with the cause (often an AggregateError, whose
// own message is empty) attached.
function describe(err: unknown): string {
  if (err instanceof AggregateError) {
    return err.errors.map(describe).join("; ");
  }
  if (err instanceof Error) {
    return err.cause ? `${err.message}: ${describe(err.cause)}` : err.message;
  }
  return String(err);
}

async function main(): Promise<number> {
  if (!token) {
    console.error("MCP_TOKEN must be set");
    return 1;
  }
  try {
    await checkHealth();
    await initialize();
    await wrongToken();
    return 0;
  } catch (err) {
    console.error(`MCP Server error: ${describe(err)}`);
    return 1;
  }
}

process.exitCode = await main();
/*
 * Ellie's Postgres Cookbook
 * Connecting to the pgEdge Postgres MCP Server, using java.net.http and
 * Jackson.
 *
 * These examples are for pgEdge Postgres MCP Server 1.1.0.
 *
 * Copyright (c) 2026, pgEdge, Inc.
 * This code is released under the PostgreSQL License.
 */

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpTimeoutException;
import java.time.Duration;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import tools.jackson.core.JacksonException;
import tools.jackson.databind.JsonNode;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.json.JsonMapper;

public class Main {

    /** Returns the value of an environment variable, or a fallback. */
    static String envOr(String name, String fallback) {
        String value = System.getenv(name);
        return (value != null && !value.isEmpty()) ? value : fallback;
    }

    static final String BASE_URL =
            envOr("MCP_URL", "http://localhost:8080").replaceAll("/+$", "");
    static final String TOKEN = envOr("MCP_TOKEN", "");

    static final Duration TIMEOUT = Duration.ofSeconds(30);

    /** Thrown when the MCP Server rejects a request or replies oddly. */
    static class McpException extends Exception {
        McpException(String message) {
            super(message);
        }
    }

    // A client and a mapper are safe to share, so the program creates
    // only one of each.
    static final HttpClient CLIENT = HttpClient.newBuilder()
            .connectTimeout(TIMEOUT)
            .build();
    static final ObjectMapper MAPPER = JsonMapper.builder().build();

    /** Returns at most the first 200 characters of a reply. */
    static String shorten(String text) {
        return text.length() > 200 ? text.substring(0, 200) : text;
    }

    /**
     * Sends a request and returns the response with its whole body. The
     * request timeout stops applying once the headers arrive, so a server
     * that stalls part of the way through the body would hang send(); this
     * applies the timeout to the whole exchange instead.
     */
    static HttpResponse<String> send(HttpRequest request)
            throws IOException, InterruptedException {
        CompletableFuture<HttpResponse<String>> pending =
                CLIENT.sendAsync(request, HttpResponse.BodyHandlers.ofString());
        try {
            return pending.get(TIMEOUT.toMillis(), TimeUnit.MILLISECONDS);
        } catch (TimeoutException e) {
            pending.cancel(true);
            throw new HttpTimeoutException("request timed out");
        } catch (ExecutionException e) {
            if (e.getCause() instanceof IOException cause) {
                throw cause;
            }
            throw new IOException(e.getCause());
        }
    }

    /** Posts a JSON-RPC message to the MCP endpoint with a token. */
    static HttpResponse<String> post(String token, Map<String, Object> message)
            throws IOException, InterruptedException {
        HttpRequest request = HttpRequest.newBuilder(URI.create(BASE_URL + "/mcp/v1"))
                .timeout(TIMEOUT)
                .header("Authorization", "Bearer " + token)
                .header("Content-Type", "application/json")
                .POST(HttpRequest.BodyPublishers.ofString(
                        MAPPER.writeValueAsString(message)))
                .build();
        return send(request);
    }

    /** Parses a reply as a JSON object, or returns null if it is not one. */
    static JsonNode parseObject(String text) {
        try {
            JsonNode node = MAPPER.readTree(text);
            return node.isObject() ? node : null;
        } catch (JacksonException e) {
            return null;
        }
    }

    /**
     * Sends a JSON-RPC request and returns its result. The server answers a
     * JSON-RPC error with HTTP status 200, so the method checks the reply
     * for an error member as well as the HTTP status.
     */
    static JsonNode rpc(String method, Map<String, Object> params)
            throws IOException, InterruptedException, McpException {
        HttpResponse<String> response = post(TOKEN, Map.of(
                "jsonrpc", "2.0", "id", 1, "method", method, "params", params));
        if (response.statusCode() != 200) {
            throw new McpException(method + " failed with HTTP "
                    + response.statusCode() + ": " + shorten(response.body()));
        }
        JsonNode reply = parseObject(response.body());
        if (reply == null || !(reply.has("result") || reply.has("error"))) {
            throw new McpException(method + " failed: unexpected response: "
                    + shorten(response.body()));
        }
        if (reply.has("error")) {
            throw new McpException(method + " failed: "
                    + reply.path("error").path("message").asString());
        }
        return reply.get("result");
    }

    /** Checks that the server is running. The /health endpoint needs no token. */
    static void checkHealth() throws IOException, InterruptedException, McpException {
        HttpRequest request = HttpRequest.newBuilder(URI.create(BASE_URL + "/health"))
                .timeout(TIMEOUT)
                .GET()
                .build();
        HttpResponse<String> response = send(request);
        if (response.statusCode() != 200) {
            throw new McpException("Health check failed with HTTP "
                    + response.statusCode() + ": " + shorten(response.body()));
        }
        JsonNode body = parseObject(response.body());
        if (body == null || !"ok".equals(body.path("status").asString(""))) {
            throw new McpException("Unexpected response: " + shorten(response.body()));
        }
        System.out.println("Health: ok");
    }

    /**
     * Introduces the client to the server, and prints the server's name and
     * version and the protocol revision that the server chose. The server
     * accepts 2024-11-05, 2025-03-26, 2025-06-18 and 2025-11-25, and answers
     * a revision it does not know with one that it does, so the client
     * checks the answer. The notifications/initialized message then
     * completes the handshake; it is a notification, with no id, so the
     * server replies with HTTP 202 and no body.
     */
    static void initialize() throws IOException, InterruptedException, McpException {
        JsonNode result = rpc("initialize", Map.of(
                "protocolVersion", "2025-06-18",
                "capabilities", Map.of(),
                "clientInfo", Map.of("name", "cookbook", "version", "1.0.0")));
        JsonNode info = result.path("serverInfo");
        if (!info.path("name").isString() || !info.path("version").isString()) {
            throw new McpException("Unexpected result: " + result);
        }
        System.out.println("Server: " + info.path("name").stringValue()
                + " " + info.path("version").stringValue());
        String protocol = result.path("protocolVersion").asString("");
        if (!protocol.equals("2025-06-18")) {
            throw new McpException("The server chose protocol version " + protocol);
        }
        System.out.println("Protocol version: " + protocol);

        HttpResponse<String> response = post(TOKEN, Map.of(
                "jsonrpc", "2.0", "method", "notifications/initialized"));
        if (response.statusCode() != 202) {
            throw new McpException("notifications/initialized failed with HTTP "
                    + response.statusCode() + ": " + shorten(response.body()));
        }
    }

    /**
     * Sends a request with a token that the server does not know, and
     * prints the HTTP status and the server's message, which is a JSON
     * object with an error member rather than a JSON-RPC reply.
     */
    static void wrongToken() throws IOException, InterruptedException, McpException {
        HttpResponse<String> response = post("not-a-real-token", Map.of(
                "jsonrpc", "2.0", "id", 1, "method", "tools/list",
                "params", Map.of()));
        if (response.statusCode() != 401) {
            throw new McpException("Expected HTTP 401 for a wrong token, but got "
                    + response.statusCode());
        }
        JsonNode body = parseObject(response.body());
        String message = body == null ? "" : body.path("error").asString("");
        if (message.isEmpty()) {
            message = "no message";
        }
        System.out.println("Wrong token: HTTP " + response.statusCode() + ", " + message);
    }

    public static void main(String[] args) {
        if (TOKEN.isEmpty()) {
            System.err.println("MCP_TOKEN must be set");
            System.exit(1);
        }
        try {
            checkHealth();
            initialize();
            wrongToken();
        // IOException covers failures to reach the server, such as a
        // refused connection or a timeout. The message of a refused
        // connection can be empty, so print the exception itself, which
        // includes its type.
        } catch (IOException | InterruptedException e) {
            System.err.println("MCP Server error: " + e);
            System.exit(1);
        } catch (McpException e) {
            System.err.println("MCP Server error: " + e.getMessage());
            System.exit(1);
        }
    }
}
/*
 * Ellie's Postgres Cookbook
 * Connecting to the pgEdge Postgres MCP Server, using HttpClient and
 * System.Text.Json.
 *
 * These examples are for pgEdge Postgres MCP Server 1.1.0.
 *
 * Copyright (c) 2026, pgEdge, Inc.
 * This code is released under the PostgreSQL License.
 */

using System.Net;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;

// Returns the value of an environment variable, or a fallback if the
// variable is unset or empty.
static string EnvOr(string name, string fallback) =>
    Environment.GetEnvironmentVariable(name) is { Length: > 0 } value
        ? value
        : fallback;

static string BaseUrl() => EnvOr("MCP_URL", "http://localhost:8080").TrimEnd('/');
static string Token() => EnvOr("MCP_TOKEN", "");

const int TimeoutSeconds = 30;

// Returns at most the first 200 characters of a reply.
static string Shorten(string text) =>
    text.Length > 200 ? text[..200] : text;

// Parses a reply as a JSON object, or returns null if it is not one.
static JsonObject? ParseObject(string text)
{
    try
    {
        return JsonNode.Parse(text) as JsonObject;
    }
    catch (JsonException)
    {
        return null;
    }
}

// Posts a JSON-RPC message to the MCP endpoint with a token, and returns
// the HTTP status and the body of the reply.
static async Task<(HttpStatusCode Status, string Body)> PostAsync(
    HttpClient client, string token, JsonObject message)
{
    using var request = new HttpRequestMessage(HttpMethod.Post, BaseUrl() + "/mcp/v1")
    {
        Content = new StringContent(
            message.ToJsonString(), Encoding.UTF8, "application/json"),
    };
    request.Headers.Add("Authorization", $"Bearer {token}");
    using var response = await client.SendAsync(request);
    return (response.StatusCode, await response.Content.ReadAsStringAsync());
}

// Sends a JSON-RPC request and returns its result. The server answers a
// JSON-RPC error with HTTP status 200, so the function checks the reply for
// an error member as well as the HTTP status.
static async Task<JsonNode> RpcAsync(HttpClient client, string method, JsonObject parameters)
{
    var (status, body) = await PostAsync(client, Token(), new JsonObject
    {
        ["jsonrpc"] = "2.0",
        ["id"] = 1,
        ["method"] = method,
        ["params"] = parameters,
    });
    if (status != HttpStatusCode.OK)
    {
        throw new McpException(
            $"{method} failed with HTTP {(int)status}: {Shorten(body)}");
    }
    var reply = ParseObject(body);
    if (reply is null || !(reply.ContainsKey("result") || reply.ContainsKey("error")))
    {
        throw new McpException($"{method} failed: unexpected response: {Shorten(body)}");
    }
    if (reply.ContainsKey("error"))
    {
        throw new McpException($"{method} failed: {reply["error"]?["message"]}");
    }
    return reply["result"] ?? throw new McpException($"{method} failed: no result");
}

// Checks that the server is running. The /health endpoint needs no token.
static async Task CheckHealthAsync(HttpClient client)
{
    using var response = await client.GetAsync(BaseUrl() + "/health");
    var body = await response.Content.ReadAsStringAsync();
    if (response.StatusCode != HttpStatusCode.OK)
    {
        throw new McpException(
            $"Health check failed with HTTP {(int)response.StatusCode}: {Shorten(body)}");
    }
    if (ParseObject(body)?["status"] is not JsonValue status ||
        status.ToString() != "ok")
    {
        throw new McpException($"Unexpected response: {Shorten(body)}");
    }
    Console.WriteLine("Health: ok");
}

// Introduces the client to the server, and prints the server's name and
// version and the protocol revision that the server chose. The server
// accepts 2024-11-05, 2025-03-26, 2025-06-18 and 2025-11-25, and answers a
// revision it does not know with one that it does, so the client checks
// the answer. The notifications/initialized message then completes the
// handshake; it is a notification, with no id, so the server replies with
// HTTP 202 and no body.
static async Task InitializeAsync(HttpClient client)
{
    var result = await RpcAsync(client, "initialize", new JsonObject
    {
        ["protocolVersion"] = "2025-06-18",
        ["capabilities"] = new JsonObject(),
        ["clientInfo"] = new JsonObject { ["name"] = "cookbook", ["version"] = "1.0.0" },
    });
    if (result["serverInfo"]?["name"] is not JsonValue name ||
        result["serverInfo"]?["version"] is not JsonValue version)
    {
        throw new McpException($"Unexpected result: {result.ToJsonString()}");
    }
    Console.WriteLine($"Server: {name} {version}");
    var protocol = result["protocolVersion"]?.ToString();
    if (protocol != "2025-06-18")
    {
        throw new McpException($"The server chose protocol version {protocol}");
    }
    Console.WriteLine($"Protocol version: {protocol}");

    var (status, body) = await PostAsync(client, Token(), new JsonObject
    {
        ["jsonrpc"] = "2.0",
        ["method"] = "notifications/initialized",
    });
    if (status != HttpStatusCode.Accepted)
    {
        throw new McpException(
            $"notifications/initialized failed with HTTP {(int)status}: {Shorten(body)}");
    }
}

// Sends a request with a token that the server does not know, and prints
// the HTTP status and the server's message, which is a JSON object with an
// error member rather than a JSON-RPC reply.
static async Task WrongTokenAsync(HttpClient client)
{
    var (status, body) = await PostAsync(client, "not-a-real-token", new JsonObject
    {
        ["jsonrpc"] = "2.0",
        ["id"] = 1,
        ["method"] = "tools/list",
        ["params"] = new JsonObject(),
    });
    if (status != HttpStatusCode.Unauthorized)
    {
        throw new McpException(
            $"Expected HTTP 401 for a wrong token, but got {(int)status}");
    }
    var message = ParseObject(body)?["error"]?.ToString();
    if (string.IsNullOrEmpty(message))
    {
        message = "no message";
    }
    Console.WriteLine($"Wrong token: HTTP {(int)status}, {message}");
}

if (Token().Length == 0)
{
    Console.Error.WriteLine("MCP_TOKEN must be set");
    return 1;
}
try
{
    // using disposes of the client, and closes its connections, when the
    // block exits, whether or not an error occurred.
    using var client = new HttpClient
    {
        Timeout = TimeSpan.FromSeconds(TimeoutSeconds),
    };
    await CheckHealthAsync(client);
    await InitializeAsync(client);
    await WrongTokenAsync(client);
}
// HttpRequestException covers failures to reach the server, such as a
// refused connection, and TaskCanceledException covers a timeout.
catch (Exception e) when (
    e is HttpRequestException or TaskCanceledException or McpException)
{
    Console.Error.WriteLine($"MCP Server error: {e.Message}");
    return 1;
}
return 0;

// C# requires type declarations to follow the top-level statements, so
// the exception that the program throws for an error reply is declared
// here, at the end.
class McpException(string message) : Exception(message);
// Ellie's Postgres Cookbook
// Connecting to the pgEdge Postgres MCP Server, using net/http.
//
// These examples are for pgEdge Postgres MCP Server 1.1.0.
//
// Copyright (c) 2026, pgEdge, Inc.
// This code is released under the PostgreSQL License.

package main

import (
    "bytes"
    "encoding/json"
    "errors"
    "fmt"
    "io"
    "net/http"
    "os"
    "strings"
    "time"
)


// envOr returns the value of an environment variable, or a fallback if
// the variable is unset or empty.
func envOr(name, fallback string) string {
    if value := os.Getenv(name); value != "" {
        return value
    }
    return fallback
}

var (
    baseURL = strings.TrimRight(envOr("MCP_URL", "http://localhost:8080"), "/")
    token   = os.Getenv("MCP_TOKEN")
)

const timeout = 30 * time.Second

// shorten returns at most the first 200 bytes of a reply.
func shorten(body []byte) string {
    if len(body) > 200 {
        body = body[:200]
    }
    return string(body)
}

// post sends a JSON-RPC message to the MCP endpoint with a token, and
// returns the HTTP status and the body of the reply.
func post(client *http.Client, withToken string, message any) (int, []byte, error) {
    payload, err := json.Marshal(message)
    if err != nil {
        return 0, nil, err
    }
    request, err := http.NewRequest(http.MethodPost, baseURL+"/mcp/v1", bytes.NewReader(payload))
    if err != nil {
        return 0, nil, err
    }
    request.Header.Set("Authorization", "Bearer "+withToken)
    request.Header.Set("Content-Type", "application/json")

    response, err := client.Do(request)
    if err != nil {
        return 0, nil, err
    }
    defer response.Body.Close()
    body, err := io.ReadAll(response.Body)
    return response.StatusCode, body, err
}

// rpc sends a JSON-RPC request and decodes its result into out. The
// server answers a JSON-RPC error with HTTP status 200, so the function
// checks the reply for an error member as well as the HTTP status.
func rpc(client *http.Client, method string, params any, out any) error {
    status, body, err := post(client, token, map[string]any{
        "jsonrpc": "2.0", "id": 1, "method": method, "params": params,
    })
    if err != nil {
        return err
    }
    if status != http.StatusOK {
        return fmt.Errorf("%s failed with HTTP %d: %s", method, status, shorten(body))
    }
    var reply struct {
        Result json.RawMessage `json:"result"`
        Error  *struct {
            Message string `json:"message"`
        } `json:"error"`
    }
    if err := json.Unmarshal(body, &reply); err != nil || (reply.Result == nil && reply.Error == nil) {
        return fmt.Errorf("%s failed: unexpected response: %s", method, shorten(body))
    }
    if reply.Error != nil {
        return fmt.Errorf("%s failed: %s", method, reply.Error.Message)
    }
    if err := json.Unmarshal(reply.Result, out); err != nil {
        return fmt.Errorf("%s failed: unexpected result: %s", method, shorten(reply.Result))
    }
    return nil
}


// checkHealth checks that the server is running. The /health endpoint
// needs no token.
func checkHealth(client *http.Client) error {
    response, err := client.Get(baseURL + "/health")
    if err != nil {
        return err
    }
    defer response.Body.Close()
    body, err := io.ReadAll(response.Body)
    if err != nil {
        return err
    }
    if response.StatusCode != http.StatusOK {
        return fmt.Errorf("health check failed with HTTP %d: %s", response.StatusCode, shorten(body))
    }
    var health struct {
        Status string `json:"status"`
    }
    if err := json.Unmarshal(body, &health); err != nil || health.Status != "ok" {
        return fmt.Errorf("unexpected response: %s", shorten(body))
    }
    fmt.Println("Health: ok")
    return nil
}


// initialize introduces the client to the server, and prints the server's
// name and version and the protocol revision that the server chose. The
// server accepts 2024-11-05, 2025-03-26, 2025-06-18 and 2025-11-25, and
// answers a revision it does not know with one that it does, so the client
// checks the answer. The notifications/initialized message then completes
// the handshake; it is a notification, with no id, so the server replies
// with HTTP 202 and no body.
func initialize(client *http.Client) error {
    var result struct {
        ProtocolVersion string `json:"protocolVersion"`
        ServerInfo      *struct {
            Name    string `json:"name"`
            Version string `json:"version"`
        } `json:"serverInfo"`
    }
    err := rpc(client, "initialize", map[string]any{
        "protocolVersion": "2025-06-18",
        "capabilities":    map[string]any{},
        "clientInfo":      map[string]string{"name": "cookbook", "version": "1.0.0"},
    }, &result)
    if err != nil {
        return err
    }
    if result.ServerInfo == nil {
        return errors.New("initialize failed: the result has no serverInfo")
    }
    fmt.Printf("Server: %s %s\n", result.ServerInfo.Name, result.ServerInfo.Version)
    if result.ProtocolVersion != "2025-06-18" {
        return fmt.Errorf("the server chose protocol version %s", result.ProtocolVersion)
    }
    fmt.Printf("Protocol version: %s\n", result.ProtocolVersion)

    status, body, err := post(client, token, map[string]any{
        "jsonrpc": "2.0", "method": "notifications/initialized",
    })
    if err != nil {
        return err
    }
    if status != http.StatusAccepted {
        return fmt.Errorf("notifications/initialized failed with HTTP %d: %s", status, shorten(body))
    }
    return nil
}


// wrongToken sends a request with a token that the server does not know,
// and prints the HTTP status and the server's message, which is a JSON
// object with an error member rather than a JSON-RPC reply.
func wrongToken(client *http.Client) error {
    status, body, err := post(client, "not-a-real-token", map[string]any{
        "jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": map[string]any{},
    })
    if err != nil {
        return err
    }
    if status != http.StatusUnauthorized {
        return fmt.Errorf("expected HTTP 401 for a wrong token, but got %d", status)
    }
    var reply struct {
        Error string `json:"error"`
    }
    message := "no message"
    if json.Unmarshal(body, &reply) == nil && reply.Error != "" {
        message = reply.Error
    }
    fmt.Printf("Wrong token: HTTP %d, %s\n", status, message)
    return nil
}


// run makes the three checks and returns any error to the caller.
func run() error {
    if token == "" {
        return errors.New("MCP_TOKEN must be set")
    }
    client := &http.Client{Timeout: timeout}
    if err := checkHealth(client); err != nil {
        return err
    }
    if err := initialize(client); err != nil {
        return err
    }
    return wrongToken(client)
}

// main reports any error returned by run() and exits with a non-zero
// status.
func main() {
    if err := run(); err != nil {
        fmt.Fprintf(os.Stderr, "MCP Server error: %v\n", err)
        os.Exit(1)
    }
}
// Ellie's Postgres Cookbook
// Connecting to the pgEdge Postgres MCP Server, using reqwest and
// serde_json.
//
// These examples are for pgEdge Postgres MCP Server 1.1.0.
//
// Copyright (c) 2026, pgEdge, Inc.
// This code is released under the PostgreSQL License.

use std::env;
use std::error::Error;
use std::process::ExitCode;
use std::time::Duration;

use reqwest::blocking::Client;
use serde_json::{json, Value};

/// Returns the value of an environment variable, or a fallback if the
/// variable is unset or empty.
fn env_or(name: &str, fallback: &str) -> String {
    env::var(name)
        .ok()
        .filter(|value| !value.is_empty())
        .unwrap_or_else(|| fallback.to_string())
}

fn base_url() -> String {
    env_or("MCP_URL", "http://localhost:8080")
        .trim_end_matches('/')
        .to_string()
}

fn token() -> String {
    env_or("MCP_TOKEN", "")
}

const TIMEOUT: Duration = Duration::from_secs(30);

/// The errors that the functions below return; a string converts into one
/// with the ? operator or into().
type BoxError = Box<dyn Error>;

/// Returns at most the first 200 characters of a reply.
fn shorten(text: &str) -> String {
    text.chars().take(200).collect()
}

/// Posts a JSON-RPC message to the MCP endpoint with a token, and returns
/// the HTTP status and the body of the reply.
fn post(client: &Client, with_token: &str, message: &Value) -> Result<(u16, String), BoxError> {
    let response = client
        .post(format!("{}/mcp/v1", base_url()))
        .bearer_auth(with_token)
        .json(message)
        .send()?;
    let status = response.status().as_u16();
    Ok((status, response.text()?))
}

/// Sends a JSON-RPC request and returns its result. The server answers a
/// JSON-RPC error with HTTP status 200, so the function checks the reply
/// for an error member as well as the HTTP status.
fn rpc(client: &Client, method: &str, params: Value) -> Result<Value, BoxError> {
    let message = json!({"jsonrpc": "2.0", "id": 1, "method": method, "params": params});
    let (status, body) = post(client, &token(), &message)?;
    if status != 200 {
        return Err(format!("{method} failed with HTTP {status}: {}", shorten(&body)).into());
    }
    let reply = match serde_json::from_str::<Value>(&body) {
        Ok(reply) if reply.get("result").is_some() || reply.get("error").is_some() => reply,
        _ => return Err(format!("{method} failed: unexpected response: {}", shorten(&body)).into()),
    };
    if let Some(error) = reply.get("error") {
        let message = error["message"].as_str().unwrap_or("unknown error");
        return Err(format!("{method} failed: {message}").into());
    }
    Ok(reply["result"].clone())
}

/// Checks that the server is running. The /health endpoint needs no token.
fn check_health(client: &Client) -> Result<(), BoxError> {
    let response = client.get(format!("{}/health", base_url())).send()?;
    let status = response.status().as_u16();
    let body = response.text()?;
    if status != 200 {
        return Err(format!("Health check failed with HTTP {status}: {}", shorten(&body)).into());
    }
    let health: Value = serde_json::from_str(&body).unwrap_or(Value::Null);
    if health["status"].as_str() != Some("ok") {
        return Err(format!("Unexpected response: {}", shorten(&body)).into());
    }
    println!("Health: ok");
    Ok(())
}

/// Introduces the client to the server, and prints the server's name and
/// version and the protocol revision that the server chose. The server
/// accepts 2024-11-05, 2025-03-26, 2025-06-18 and 2025-11-25, and answers a
/// revision it does not know with one that it does, so the client checks
/// the answer. The notifications/initialized message then completes the
/// handshake; it is a notification, with no id, so the server replies with
/// HTTP 202 and no body.
fn initialize(client: &Client) -> Result<(), BoxError> {
    let result = rpc(
        client,
        "initialize",
        json!({
            "protocolVersion": "2025-06-18",
            "capabilities": {},
            "clientInfo": {"name": "cookbook", "version": "1.0.0"},
        }),
    )?;
    let info = &result["serverInfo"];
    let (Some(name), Some(version)) = (info["name"].as_str(), info["version"].as_str()) else {
        return Err(format!("Unexpected result: {result}").into());
    };
    println!("Server: {name} {version}");
    let protocol = result["protocolVersion"].as_str().unwrap_or("");
    if protocol != "2025-06-18" {
        return Err(format!("The server chose protocol version {protocol}").into());
    }
    println!("Protocol version: {protocol}");

    let message = json!({"jsonrpc": "2.0", "method": "notifications/initialized"});
    let (status, body) = post(client, &token(), &message)?;
    if status != 202 {
        return Err(format!(
            "notifications/initialized failed with HTTP {status}: {}",
            shorten(&body)
        )
        .into());
    }
    Ok(())
}

/// Sends a request with a token that the server does not know, and prints
/// the HTTP status and the server's message, which is a JSON object with
/// an error member rather than a JSON-RPC reply.
fn wrong_token(client: &Client) -> Result<(), BoxError> {
    let message = json!({"jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {}});
    let (status, body) = post(client, "not-a-real-token", &message)?;
    if status != 401 {
        return Err(format!("Expected HTTP 401 for a wrong token, but got {status}").into());
    }
    let reply: Value = serde_json::from_str(&body).unwrap_or(Value::Null);
    let message = match reply["error"].as_str() {
        Some(text) if !text.is_empty() => text,
        _ => "no message",
    };
    println!("Wrong token: HTTP {status}, {message}");
    Ok(())
}

/// Makes the three checks. The ? operator returns any error to the
/// caller.
fn run() -> Result<(), BoxError> {
    if token().is_empty() {
        return Err("MCP_TOKEN must be set".into());
    }
    let client = Client::builder().timeout(TIMEOUT).build()?;
    check_health(&client)?;
    initialize(&client)?;
    wrong_token(&client)
}

/// Reports any error returned by run() and exits with a failure status.
/// The top-level error is often generic ("error sending request"), so the
/// chain of underlying causes is printed too.
fn main() -> ExitCode {
    match run() {
        Ok(()) => ExitCode::SUCCESS,
        Err(e) => {
            eprintln!("MCP Server error: {e}");
            let mut cause = e.source();
            while let Some(c) = cause {
                eprintln!("  caused by: {c}");
                cause = c.source();
            }
            ExitCode::FAILURE
        }
    }
}
# Ellie's Postgres Cookbook
# Connecting to the pgEdge Postgres MCP Server, using Net::HTTP.
#
# These examples are for pgEdge Postgres MCP Server 1.1.0.
#
# Copyright (c) 2026, pgEdge, Inc.
# This code is released under the PostgreSQL License.

require "json"
require "net/http"
require "uri"

# Returns the value of an environment variable, or a fallback if the
# variable is unset or empty.
def env_or(name, fallback)
  value = ENV[name]
  value.nil? || value.empty? ? fallback : value
end

BASE_URL = env_or("MCP_URL", "http://localhost:8080").sub(%r{/+\z}, "")
TOKEN = env_or("MCP_TOKEN", "")

TIMEOUT_SECONDS = 30

# Raised when the MCP Server rejects a request or replies oddly.
class McpError < StandardError; end

# Parses a reply as a JSON object, or returns nil if it is not one.
def parse_object(text)
  body = JSON.parse(text.to_s)
  body.is_a?(Hash) ? body : nil
rescue JSON::ParserError
  nil
end

# Posts a JSON-RPC message to the MCP endpoint with a token, and returns
# the response.
def post(http, token, message)
  request = Net::HTTP::Post.new(URI("#{BASE_URL}/mcp/v1"))
  request["Authorization"] = "Bearer #{token}"
  request["Content-Type"] = "application/json"
  request.body = JSON.generate(message)
  http.request(request)
end

# Sends a JSON-RPC request and returns its result. The server answers a
# JSON-RPC error with HTTP status 200, so the method checks the reply for
# an error member as well as the HTTP status.
def rpc(http, method, params)
  response = post(http, TOKEN, { jsonrpc: "2.0", id: 1, method: method, params: params })
  unless response.code == "200"
    raise McpError, "#{method} failed with HTTP #{response.code}: #{response.body.to_s[0, 200]}"
  end

  reply = parse_object(response.body)
  unless reply && (reply.key?("result") || reply.key?("error"))
    raise McpError, "#{method} failed: unexpected response: #{response.body.to_s[0, 200]}"
  end
  raise McpError, "#{method} failed: #{reply["error"]["message"]}" if reply.key?("error")

  reply["result"]
end

# Checks that the server is running. The /health endpoint needs no token.
def check_health(http)
  response = http.request(Net::HTTP::Get.new(URI("#{BASE_URL}/health")))
  unless response.code == "200"
    raise McpError, "Health check failed with HTTP #{response.code}: #{response.body.to_s[0, 200]}"
  end
  unless parse_object(response.body)&.fetch("status", nil) == "ok"
    raise McpError, "Unexpected response: #{response.body.to_s[0, 200]}"
  end

  puts "Health: ok"
end

# Introduces the client to the server, and prints the server's name and
# version and the protocol revision that the server chose. The server
# accepts 2024-11-05, 2025-03-26, 2025-06-18 and 2025-11-25, and answers a
# revision it does not know with one that it does, so the client checks
# the answer. The notifications/initialized message then completes the
# handshake; it is a notification, with no id, so the server replies with
# HTTP 202 and no body.
def initialize_session(http)
  result = rpc(http, "initialize", {
                 protocolVersion: "2025-06-18",
                 capabilities: {},
                 clientInfo: { name: "cookbook", version: "1.0.0" }
               })
  info = result.is_a?(Hash) ? result["serverInfo"] : nil
  unless info.is_a?(Hash) && info["name"].is_a?(String) && info["version"].is_a?(String)
    raise McpError, "Unexpected result: #{result}"
  end

  puts "Server: #{info["name"]} #{info["version"]}"
  protocol = result["protocolVersion"]
  raise McpError, "The server chose protocol version #{protocol}" unless protocol == "2025-06-18"

  puts "Protocol version: #{protocol}"

  response = post(http, TOKEN, { jsonrpc: "2.0", method: "notifications/initialized" })
  return if response.code == "202"

  raise McpError, "notifications/initialized failed with HTTP #{response.code}: " \
                  "#{response.body.to_s[0, 200]}"
end

# Sends a request with a token that the server does not know, and prints
# the HTTP status and the server's message, which is a JSON object with an
# error member rather than a JSON-RPC reply.
def wrong_token(http)
  response = post(http, "not-a-real-token",
                  { jsonrpc: "2.0", id: 1, method: "tools/list", params: {} })
  unless response.code == "401"
    raise McpError, "Expected HTTP 401 for a wrong token, but got #{response.code}"
  end

  message = parse_object(response.body)&.fetch("error", nil)
  message = "no message" if message.nil? || message.to_s.empty?
  puts "Wrong token: HTTP #{response.code}, #{message}"
end

if TOKEN.empty?
  warn "MCP_TOKEN must be set"
  exit 1
end

begin
  uri = URI(BASE_URL)
  # start opens a connection, and closes it when the block exits, whether
  # or not an error occurred.
  Net::HTTP.start(
    uri.host, uri.port,
    use_ssl: uri.scheme == "https",
    open_timeout: TIMEOUT_SECONDS, read_timeout: TIMEOUT_SECONDS
  ) do |http|
    check_health(http)
    initialize_session(http)
    wrong_token(http)
  end
# SystemCallError covers a refused connection, Timeout::Error covers the
# connection and read timeouts, and SocketError covers an unknown host.
rescue McpError, SystemCallError, Timeout::Error, SocketError, IOError => e
  warn "MCP Server error: #{e.message}"
  exit 1
end
#!/usr/bin/env bash
#
# Ellie's Postgres Cookbook
# Connecting to the pgEdge Postgres MCP Server, using curl and jq.
#
# These examples are for pgEdge Postgres MCP Server 1.1.0.
#
# Copyright (c) 2026, pgEdge, Inc.
# This code is released under the PostgreSQL License.

# Stop at the first failed command or unset variable, and treat a failure
# anywhere in a pipeline as a failure of the whole pipeline.
set -euo pipefail

# The script needs curl to make the requests and jq to build the requests
# and read the replies.
for tool in curl jq; do
    command -v "$tool" >/dev/null || { echo "$tool is required" >&2; exit 1; }
done

# Read the settings from the environment. The :- and :? forms, unlike - and
# ?, treat an empty variable as unset.
MCP_URL="${MCP_URL:-http://localhost:8080}"
MCP_URL="${MCP_URL%/}"
: "${MCP_TOKEN:?MCP_TOKEN must be set}"

TIMEOUT_SECONDS=30

# Posts a JSON-RPC message to the MCP endpoint with a token, and prints the
# reply followed by the HTTP status on a line of its own: post TOKEN BODY.
# The token goes to curl in a header file that bash supplies through a
# pipe, so that it never appears in the arguments of the curl process, which
# other users can read in the process list.
post() {
    curl --silent --show-error \
        --connect-timeout 10 --max-time "$TIMEOUT_SECONDS" \
        --header @<(printf 'Authorization: Bearer %s\n' "$1") \
        --header 'Content-Type: application/json' \
        --write-out '\n%{http_code}' --data-binary "$2" "$MCP_URL/mcp/v1"
}

# Sends a JSON-RPC request and prints its result: rpc METHOD PARAMS_JSON.
# The server answers a JSON-RPC error with HTTP status 200, so the function
# checks the reply for an error member as well as the HTTP status.
rpc() {
    local method="$1" request reply status body
    request=$(jq -nc --arg method "$method" --argjson params "$2" \
        '{jsonrpc: "2.0", id: 1, method: $method, params: $params}')
    reply=$(post "$MCP_TOKEN" "$request") || return 1
    status="${reply##*$'\n'}"
    body="${reply%$'\n'*}"
    if [[ "$status" != 200 ]]; then
        echo "$method failed with HTTP $status: ${body:0:200}" >&2
        return 1
    fi
    if ! jq -e 'has("result") or has("error")' >/dev/null 2>&1 <<<"$body"; then
        echo "$method failed: unexpected response: ${body:0:200}" >&2
        return 1
    fi
    if jq -e 'has("error")' >/dev/null <<<"$body"; then
        echo "$method failed: $(jq -r '.error.message' <<<"$body")" >&2
        return 1
    fi
    jq -c '.result' <<<"$body"
}

# Checks that the server is running. The /health endpoint needs no token.
check_health() {
    local body
    body=$(curl --silent --show-error --fail-with-body \
        --connect-timeout 10 --max-time "$TIMEOUT_SECONDS" \
        "$MCP_URL/health") || {
        echo "Health check failed: $body" >&2
        return 1
    }
    jq -er 'if .status == "ok" then "Health: ok"
            else error("status is not ok") end' <<<"$body" 2>/dev/null || {
        echo "Unexpected response: ${body:0:200}" >&2
        return 1
    }
}

# Introduces the client to the server, and prints the server's name and
# version and the protocol revision that the server chose. The server
# accepts 2024-11-05, 2025-03-26, 2025-06-18 and 2025-11-25, and answers a
# revision it does not know with one that it does, so the client checks
# the answer. The notifications/initialized message then completes the
# handshake; it is a notification, with no id, so the server replies with
# HTTP 202 and no body.
initialize() {
    local result protocol reply
    result=$(rpc initialize '{
        "protocolVersion": "2025-06-18",
        "capabilities": {},
        "clientInfo": {"name": "cookbook", "version": "1.0.0"}
    }') || return 1
    jq -r '"Server: \(.serverInfo.name) \(.serverInfo.version)"' <<<"$result"
    protocol=$(jq -r '.protocolVersion' <<<"$result")
    if [[ "$protocol" != "2025-06-18" ]]; then
        echo "The server chose protocol version $protocol" >&2
        return 1
    fi
    echo "Protocol version: $protocol"

    reply=$(post "$MCP_TOKEN" \
        '{"jsonrpc": "2.0", "method": "notifications/initialized"}') || return 1
    if [[ "${reply##*$'\n'}" != 202 ]]; then
        echo "notifications/initialized failed: ${reply:0:200}" >&2
        return 1
    fi
}

# Sends a request with a token that the server does not know, and prints
# the HTTP status and the server's message, which is a JSON object with an
# error member rather than a JSON-RPC reply.
wrong_token() {
    local reply status body
    reply=$(post "not-a-real-token" \
        '{"jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {}}') \
        || return 1
    status="${reply##*$'\n'}"
    body="${reply%$'\n'*}"
    if [[ "$status" != 401 ]]; then
        echo "Expected HTTP 401 for a wrong token, but got $status" >&2
        return 1
    fi
    echo "Wrong token: HTTP $status, $(jq -r '.error // "no message"' <<<"$body")"
}

main() {
    check_health
    initialize
    wrong_token
}

main

Next Steps

The following resources provide more information about the topics in this recipe: