Skip to main content
Version: ERPBridge + bridgectl ยท v0.5.0-alpha.2

MCP client guide (Python and TypeScript)

Who is this guide for? This guide is for developers who are new to MCP (Model Context Protocol). It shows how to build clients that connect to ERPBridge Server over Streamable HTTP or stdio, with complete examples in Python and TypeScript.

What is MCP?โ€‹

MCP (Model Context Protocol) is a standard protocol that lets clients call tools, read resources, and use prompts exposed by a server โ€” designed for AI and automation workflows.

In this guide, the server is ERPBridge, which exposes business tools (like listing invoices) over MCP. Your job as a client is to:

  1. Connect to the server and start a session.
  2. Discover which tools, resources, and prompts are available.
  3. Interact with them and receive results.

All communication uses JSON-RPC 2.0 โ€” a simple, human-readable message format.

Prerequisitesโ€‹

Start ERPBridge Serverโ€‹

Before writing any client code, you need a running ERPBridge Server to connect to.

Start with Docker
docker compose up -d --build
tip

If you are unsure which option to choose, use Docker. Docker handles all dependencies automatically.

Verify that the server is runningโ€‹

Once started, the server listens at:

http://localhost:8080/mcp/

You can change the port with the MCP_PORT environment variable or the full address with BASE_URL.

Quick check โ€” confirm the server responds:

Health check via initialize
curl -s -o /dev/null -w "%{http_code}" -X POST http://localhost:8080/mcp/ \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"0.1.0"}}}'

You see 200.

Install language dependenciesโ€‹

Install requests
pip install requests

Choosing a transportโ€‹

MCP supports two ways to communicate with the server:

TransportHow it worksBest for
Streamable HTTPSend JSON-RPC messages over regular HTTP POST requests. Optionally receive server-sent events (SSE) for real-time notifications.Web apps, services, API testing tools like Postman
stdioSpawn the server as a child process and communicate over its standard input/output (stdin/stdout).Local scripts, CLI tools, desktop integrations

Not sure which to pick? Start with Streamable HTTP. It is easier to debug because you can inspect requests in a browser or Postman.

How Streamable HTTP worksโ€‹

Every message you send follows the JSON-RPC 2.0 format and is sent as a POST to /mcp/. Here's the typical flow:

Request sequence
Client ERPBridge Server
| |
|--- POST /mcp/ (initialize) ------->|
|<-- 200 OK + Mcp-Session-Id --------| โ† Save this header!
| |
|--- POST /mcp/ (tools/list) ------->| โ† Include session ID
|<-- List of available tools --------|
| |
|--- POST /mcp/ (resources/list) ---->|
|<-- List of available resources ----|
| |
|--- POST /mcp/ (tools/call) ------->| โ† Call a specific tool
|<-- Tool result ------------------- |
| |
|--- GET /mcp/ (SSE, optional) ----->| โ† Subscribe to notifications
|<-- Server-sent events (ongoing) ---|

Step 1 โ€” Initialize the sessionโ€‹

Send an initialize request first. The server replies with a Mcp-Session-Id header that you must include in all subsequent requests.

initialize request
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-03-26",
"capabilities": {
"logging": {},
"prompts": {},
"resources": {},
"tools": {}
},
"clientInfo": { "name": "my-client", "version": "0.1.0" }
}
}
Why is the session ID important?

The server uses it to associate your requests with your session. Without it, subsequent calls will fail.

Python client โ€” Streamable HTTPโ€‹

Minimal working exampleโ€‹

This example connects to ERPBridge, lists available tools, and calls one.

python_http_client.py
import json
import requests

BASE_URL = "http://localhost:8080/mcp/"

# --- Step 1: Initialize the session ---
init_payload = {
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-03-26",
"capabilities": {"tools": {}, "resources": {}, "prompts": {}},
"clientInfo": {"name": "python-client", "version": "0.1.0"},
},
}

session = requests.Session()
init_resp = session.post(BASE_URL, json=init_payload)
init_resp.raise_for_status()

# Save the session ID โ€” required for all future requests
session_id = init_resp.headers.get("Mcp-Session-Id")
if not session_id:
raise RuntimeError("Server did not return Mcp-Session-Id. Did initialize succeed?")

headers = {"Mcp-Session-Id": session_id}
print(f"Session started: {session_id}")

# --- Step 2: List available tools ---
tools_resp = session.post(
BASE_URL,
headers=headers,
json={"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}},
)
tools_resp.raise_for_status()
tools = tools_resp.json()

print("Available tools:")
for tool in tools.get("result", {}).get("tools", []):
print(f" - {tool['name']}: {tool.get('description', 'No description')}")

# --- Step 3: Call a tool ---
call_resp = session.post(
BASE_URL,
headers=headers,
json={
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "list_purchase_invoices",
"arguments": {},
},
},
)
call_resp.raise_for_status()
result = call_resp.json()

print("\nTool result:")
print(json.dumps(result, indent=2))

Python client โ€” stdioโ€‹

Use this approach when you want to run ERPBridge Server as a subprocess and communicate directly through its standard input/output.

Minimal Working Exampleโ€‹

python_stdio_client.py
import json
import subprocess

# --- Start the server as a child process ---
proc = subprocess.Popen(
["go", "run", "services/erpbridge-server/main.go", "--stdio"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE, # Capture stderr so it doesn't pollute your output
text=True,
)

def send(message: dict):
"""Send a JSON-RPC message to the server via stdin."""
proc.stdin.write(json.dumps(message) + "\n")
proc.stdin.flush()

def receive() -> dict:
"""Read one JSON-RPC response from the server via stdout."""
line = proc.stdout.readline()
if not line:
raise RuntimeError("Server process closed unexpectedly.")
return json.loads(line)

# --- Step 1: Initialize ---
send({
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-03-26",
"capabilities": {"tools": {}, "resources": {}, "prompts": {}},
"clientInfo": {"name": "python-stdio", "version": "0.1.0"},
},
})
init_response = receive()
print("Initialized:", json.dumps(init_response, indent=2))

# --- Step 2: List tools ---
send({"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}})
tools_response = receive()
print("Tools:", json.dumps(tools_response, indent=2))

# --- Step 3: Call a tool ---
send({
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "list_purchase_invoices",
"arguments": {},
},
})
result = receive()
print("Result:", json.dumps(result, indent=2))

# --- Always clean up ---
proc.stdin.close()
proc.wait()

TypeScript client โ€” Streamable HTTPโ€‹

Minimal working exampleโ€‹

ts_http_client.ts
const BASE_URL = "http://localhost:8080/mcp/";

async function main() {
// --- Step 1: Initialize the session ---
const initResp = await fetch(BASE_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
jsonrpc: "2.0",
id: 1,
method: "initialize",
params: {
protocolVersion: "2025-03-26",
capabilities: { tools: {}, resources: {}, prompts: {} },
clientInfo: { name: "ts-client", version: "0.1.0" },
},
}),
});

if (!initResp.ok) {
throw new Error(`Initialize failed: ${initResp.status} ${initResp.statusText}`);
}

// Save the session ID โ€” required for all future requests
const sessionId = initResp.headers.get("Mcp-Session-Id");
if (!sessionId) {
throw new Error("Server did not return Mcp-Session-Id. Did initialize succeed?");
}

const headers = {
"Content-Type": "application/json",
"Mcp-Session-Id": sessionId,
};

console.log(`Session started: ${sessionId}`);

// --- Step 2: List available tools ---
const toolsResp = await fetch(BASE_URL, {
method: "POST",
headers,
body: JSON.stringify({
jsonrpc: "2.0",
id: 2,
method: "tools/list",
params: {},
}),
});

const tools = await toolsResp.json();
console.log("Available tools:", JSON.stringify(tools, null, 2));

// --- Step 3: Call a tool ---
const callResp = await fetch(BASE_URL, {
method: "POST",
headers,
body: JSON.stringify({
jsonrpc: "2.0",
id: 3,
method: "tools/call",
params: {
name: "list_purchase_invoices",
arguments: {},
},
}),
});

const result = await callResp.json();
console.log("Tool result:", JSON.stringify(result, null, 2));
}

main().catch(console.error);

TypeScript client โ€” stdioโ€‹

ts_stdio_client.ts
import { spawn } from "node:child_process";
import * as readline from "node:readline";

// --- Start the server as a child process ---
const proc = spawn(
"go",
["run", "services/erpbridge-server/main.go", "--stdio"],
{
stdio: ["pipe", "pipe", "inherit"], // inherit stderr so errors are visible
}
);

// Use readline to read one line at a time from stdout
const rl = readline.createInterface({ input: proc.stdout });
const pendingReads: Array<(line: string) => void> = [];

rl.on("line", (line) => {
const resolve = pendingReads.shift();
if (resolve) resolve(line);
});

function send(message: object): void {
proc.stdin.write(JSON.stringify(message) + "\n");
}

function receive(): Promise<object> {
return new Promise((resolve) => {
pendingReads.push((line) => resolve(JSON.parse(line)));
});
}

async function main() {
// --- Step 1: Initialize ---
send({
jsonrpc: "2.0",
id: 1,
method: "initialize",
params: {
protocolVersion: "2025-03-26",
capabilities: { tools: {}, resources: {}, prompts: {} },
clientInfo: { name: "ts-stdio", version: "0.1.0" },
},
});

const initResponse = await receive();
console.log("Initialized:", JSON.stringify(initResponse, null, 2));

// --- Step 2: List tools ---
send({ jsonrpc: "2.0", id: 2, method: "tools/list", params: {} });
const toolsResponse = await receive();
console.log("Tools:", JSON.stringify(toolsResponse, null, 2));

// --- Step 3: Call a tool ---
send({
jsonrpc: "2.0",
id: 3,
method: "tools/call",
params: {
name: "list_purchase_invoices",
arguments: {},
},
});

const result = await receive();
console.log("Result:", JSON.stringify(result, null, 2));

// --- Always clean up ---
proc.stdin.end();
}

main().catch(console.error);

Working with resourcesโ€‹

Resources are read-only data sources (like database records or documentation) that the AI can fetch.

List Resourcesโ€‹

resources/list
{ "jsonrpc": "2.0", "id": 5, "method": "resources/list", "params": {} }

Read a Resourceโ€‹

resources/read
{
"jsonrpc": "2.0",
"id": 6,
"method": "resources/read",
"params": {
"uri": "mcp://finance/invoices/INV-2024-001"
}
}

Working with promptsโ€‹

Prompts are predefined instruction templates that help AI agents perform specific tasks.

List Promptsโ€‹

prompts/list
{ "jsonrpc": "2.0", "id": 7, "method": "prompts/list", "params": {} }

Get a Promptโ€‹

prompts/get
{
"jsonrpc": "2.0",
"id": 8,
"method": "prompts/get",
"params": {
"name": "analyze-spending",
"arguments": {
"department": "Engineering"
}
}
}

Completion and suggestionsโ€‹

ERPBridge provides suggestions for tool, resource, and prompt arguments. This is useful for building interactive CLIs or UI components.

Get Suggestionsโ€‹

completion/complete
{
"jsonrpc": "2.0",
"id": 9,
"method": "completion/complete",
"params": {
"ref": {
"type": "ref/resource",
"uri": "mcp://finance/invoices/"
},
"argument": {
"name": "invoice_id",
"value": "INV-"
}
}
}

Hot reloading and lifecycleโ€‹

ERPBridge supports Hot Reloading and dynamic tool lifecycle management. If you add, modify, or delete tools via the bridgectl CLI, the server automatically updates its registry and notifies all active sessions.

Standard Sync Notificationโ€‹

When tools are added or removed, the server sends a standard notification:

  • Method: notifications/tools/list_changed
  • Action: Re-call tools/list to get the updated set.

ERPBridge Custom Notificationsโ€‹

For more granular control, ERPBridge sends specific lifecycle events:

notifications/tool_deletedโ€‹

Sent when a tool is deactivated (soft-deleted) from the registry.

tool_deleted notification
{
"method": "notifications/tool_deleted",
"params": {
"name": "finance.get_invoice",
"version": "1.0.0",
"reason": "deregistered from registry"
}
}

notifications/messageโ€‹

Used for system-wide alerts or administrative broadcasts.

message notification
{
"method": "notifications/message",
"params": {
"message": "Server will undergo maintenance in 5 minutes",
"type": "system"
}
}

notifications/progressโ€‹

Sent in real time by long-running tools. Each event reports one step of the operation.

progress notification
{
"method": "notifications/progress",
"params": {
"progress": 3,
"total": 10,
"message": "Processing step 3/10..."
}
}

notifications/alertโ€‹

Sent when a tool call fails or a condition needs attention.

alert notification
{
"method": "notifications/alert",
"params": {
"message": "Upstream ERP request failed: transient erp error: 503",
"severity": "warning"
}
}

Built-in system toolsโ€‹

The server registers two demonstration tools that are always available. Use them to verify client behavior before you connect a real ERP.

system.progress_testโ€‹

Emits one notifications/progress event every 200ms so you can verify your client renders progress updates over the SSE stream.

Call system.progress_test
{
"jsonrpc": "2.0",
"id": 11,
"method": "tools/call",
"params": {
"name": "system.progress_test",
"arguments": { "steps": 5 }
}
}
ArgumentTypeDefaultDescription
stepsinteger10Number of steps to simulate. Max 100.

system.sensitive_log_testโ€‹

Emits a log line containing the token and message you pass in. The logger redacts the token before it reaches your client, so this is a quick way to verify redaction works end to end.

Call system.sensitive_log_test
{
"jsonrpc": "2.0",
"id": 12,
"method": "tools/call",
"params": {
"name": "system.sensitive_log_test",
"arguments": { "token": "sk-secret-value", "message": "hello" }
}
}
ArgumentTypeDescription
tokenstringA sensitive value that must be redacted from logs.
messagestringA normal message that stays visible.

Error handlingโ€‹

JSON-RPC errors are returned inside the response body, not as HTTP error codes. Always check the response for an error field.

What a JSON-RPC error looks likeโ€‹

JSON-RPC error response
{
"jsonrpc": "2.0",
"id": 3,
"error": {
"code": -32601,
"message": "Method not found",
"data": "No tool named 'finance.bad_tool_name'"
}
}

Common JSON-RPC error codesโ€‹

CodeMeaningFix
-32700Parse errorYour request body contains invalid JSON
-32600Invalid requestMissing jsonrpc, id, or method fields
-32601Method not foundThe tool name does not exist. Use tools/list to find it.
-32602Invalid paramsWrong or missing arguments for the tool
-32603Internal errorServer-side error โ€” check server logs

Preserve the MCP result envelopeโ€‹

An MCP tools/call response is a JSON-RPC result. Read the content array and preserve any structured result fields supplied by the SDK. ERPBridge may place a JSON-encoded ERPBridge compatibility result inside a text content item, so content[0].text is not automatically the complete protocol response.

The direct REST endpoint, /api/tools/invoke, is a registry-only compatibility endpoint and returns the legacy ToolResult shape. MCP built-in tools whose names start with system. are available through MCP, not through that REST endpoint.

Troubleshootingโ€‹

Mcp-Session-Id is missing from the responseโ€‹

Cause: The session wasn't initialized properly.

Fix:

  • Make sure your very first request uses "method": "initialize".
  • Make sure that you send the request to POST /mcp/, not GET or /mcp without the trailing slash.

404 Not Found or connection refusedโ€‹

Cause: The server is not running, or the URL is incorrect.

Fix:

  • Confirm the server started successfully (check Docker logs or terminal output).
  • Verify the port: curl http://localhost:8080/mcp/

Logging and redactionโ€‹

ERPBridge implements standard MCP logging via notifications/message.

Setting the log levelโ€‹

logging/setLevel
{
"jsonrpc": "2.0",
"id": 10,
"method": "logging/setLevel",
"params": { "level": "debug" }
}

Available levels: debug, info, notice, warning, error, critical, alert, emergency.

Automatic redactionโ€‹

For security, ERPBridge automatically redacts sensitive data (API keys, passwords, PII) from all logs sent to clients. Redacted fields appear as [REDACTED].

Quick reference cheat sheetโ€‹

JSON-RPC message templateโ€‹

Message template
{
"jsonrpc": "2.0",
"id": <any unique integer>,
"method": "<method name>",
"params": {}
}

Request sequence (HTTP)โ€‹

Request order
1. POST /mcp/ โ†’ initialize (no session ID needed)
2. POST /mcp/ โ†’ tools/list (include Mcp-Session-Id)
3. POST /mcp/ โ†’ resources/list (include Mcp-Session-Id)
4. POST /mcp/ โ†’ prompts/list (include Mcp-Session-Id)
5. POST /mcp/ โ†’ tools/call (include Mcp-Session-Id)
6. GET /mcp/ โ†’ SSE stream (optional, include Mcp-Session-Id)

Transport at a glanceโ€‹

Streamable HTTPstdio
Session IDRequired (from initialize response header)Not used
NotificationsVia SSE (GET stream)Supported (via stdout)
DebuggingEasy (curl, Postman, browser devtools)Harder
Best forServices, web appsLocal scripts, CLIs

Key headersโ€‹

HeaderWhen to use
Content-Type: application/jsonAll POST requests
Mcp-Session-Id: <id>All requests after initialize
Accept: text/event-streamSSE / notifications GET request