Skip to content

HTTP requests and MCP clients

This guide uses DemoServer and the /mcp/demo endpoint from Servers and routes. Register that server and start Orionis with orionis serve or python -B reactor serve. Examples use http://localhost:8000; adjust the host and port to your application’s console output.

Send every operation as POST to the same endpoint. Listing tools does not create /mcp/demo/tools or use GET: send method: "tools/list" in the JSON-RPC body.

OperationMCP methodParameters besides _meta
Discover the serverserver/discoverNone.
List toolstools/listOptional cursor.
Invoke a tooltools/callname and arguments.
List fixed resourcesresources/listOptional cursor.
List URI templatesresources/templates/listOptional cursor.
Read a resourceresources/readuri.
List promptsprompts/listOptional cursor.
Get a promptprompts/getname and arguments with string values.
Complete an argumentcompletion/completeref, argument, and optional context; see resources and prompts.
Listen for changessubscriptions/listennotifications; see subscriptions.

server/discover inspects capabilities but does not initialize a session or need to precede each operation. Orionis implements only MCP 2026-07-28.

ElementValue and purpose
Content-TypeA single application/json header; its charset parameter is accepted.
AcceptExplicitly include application/json, text/event-stream with positive quality for both. */* does not replace them.
MCP-Protocol-Version2026-07-28, identical to the revision in params._meta.
MCP-MethodThe same string as method in the envelope.
MCP-NameFor tools/call and prompts/get, the name; for resources/read, the complete uri. Not needed for lists.
params._metaInclude io.modelcontextprotocol/protocolVersion and io.modelcontextprotocol/clientCapabilities in every request.

Tools whose schema declares x-mcp-header also require those arguments to be mirrored in their declared headers. The transport checks values before executing the handler. Do not send an MCP session header: every request is processed independently.

Save this UTF-8 body as discover.json:

{
"jsonrpc": "2.0",
"id": "discover-1",
"method": "server/discover",
"params": {
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}

Run from the directory containing the file. On Windows, use the curl.exe executable; on other systems change its name to curl:

Terminal window
curl.exe --request POST http://localhost:8000/mcp/demo --header "Content-Type: application/json" --header "Accept: application/json, text/event-stream" --header "MCP-Protocol-Version: 2026-07-28" --header "MCP-Method: server/discover" --data-binary "@discover.json"

The response retains id: "discover-1" and includes result.supportedVersions, result.capabilities, and the server identity in result._meta["io.modelcontextprotocol/serverInfo"]. Completed responses include resultType: "complete"; cacheable operations also include ttlMs and cacheScope.

Send a string or integer id for requests expecting a result. A notification without id receives HTTP 202 with no body; it does not replace a tools/call request that must execute and return a result.

To test finite operations from Node.js with fetch available, create mcp-http.mjs and copy this function. Append the query blocks to the same file and run node mcp-http.mjs:

const endpoint = process.env.MCP_URL || "http://localhost:8000/mcp/demo";
let nextId = 1;
async function rpc(method, params = {}) {
const id = nextId++;
const headers = {
"Content-Type": "application/json",
"Accept": "application/json, text/event-stream",
"MCP-Protocol-Version": "2026-07-28",
"MCP-Method": method,
};
if (["tools/call", "prompts/get", "resources/read"].includes(method)) {
headers["MCP-Name"] = method === "resources/read" ? params.uri : params.name;
}
const response = await fetch(endpoint, {
method: "POST",
headers,
signal: AbortSignal.timeout(15000),
body: JSON.stringify({
jsonrpc: "2.0",
id,
method,
params: {
...params,
_meta: {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {},
},
},
}),
});
if (!response.headers.get("content-type")?.startsWith("application/json")) {
await response.body?.cancel();
throw new Error("Expected a finite JSON response");
}
const envelope = await response.json();
if (!response.ok || envelope.error) {
throw new Error(JSON.stringify({ status: response.status, error: envelope.error }));
}
if (envelope.id !== id) {
throw new Error("Unexpected response ID");
}
return envelope.result;
}

The function mirrors method and name/URI headers, adds metadata, and checks HTTP, JSON-RPC, and response identity. It uses the example’s ASCII identifiers and handles finite JSON responses. Adapt the transport for tools requiring custom headers or SSE streams.

const server = await rpc("server/discover");
console.log(server.supportedVersions); // ["2026-07-28"]
console.log(server.capabilities);
console.log(server._meta["io.modelcontextprotocol/serverInfo"]);

Metadata describes client capabilities for each call. The empty object declares no additional capabilities; it does not enable elicitation or subscriptions by itself.

const tools = await rpc("tools/list");
console.log(tools.tools);
const greeting = await rpc("tools/call", {
name: "greet",
arguments: { name: "Ada" },
});
console.log(greeting.structuredContent); // Hello, Ada!
console.log(greeting.isError === true);

tools/list returns descriptions and schemas without executing tools. tools/call executes greet with its validated payload. A business result with isError: true differs from envelope.error: inspect both as appropriate. See Tools.

const resources = await rpc("resources/list");
console.log(resources.resources);
const templates = await rpc("resources/templates/list");
console.log(templates.resourceTemplates);
const content = await rpc("resources/read", {
uri: "demo://greetings/Ada",
});
console.log(content.contents);

In the basic example, GreetingResource declares a template: it appears in resourceTemplates and the fixed resource list may be empty. resources/read receives the concrete URI, rather than demo://greetings/{name}. A resource is not an HTTP URL to open in a browser and does not grant filesystem access. See Resources.

const prompts = await rpc("prompts/list");
console.log(prompts.prompts);
const prompt = await rpc("prompts/get", {
name: "greeting",
arguments: { name: "Ada" },
});
console.log(prompt.messages);

prompts/list describes names and arguments; prompts/get returns messages with their roles and content blocks. Getting a prompt does not execute a model. Argument values are strings even when their text represents a number. See Prompts.

Add this helper when a catalog has more entries than fit in one page:

async function listAll(method, field) {
const entries = [];
let cursor;
do {
const page = await rpc(method, cursor === undefined ? {} : { cursor });
entries.push(...page[field]);
cursor = page.nextCursor;
} while (cursor !== undefined);
return entries;
}
console.log(await listAll("tools/list", "tools"));

nextCursor is opaque: return it unchanged to the same server and method while retaining credentials. Page size comes from default_page_size; requests do not offer limit or page_size. Restart listing if catalog or access changes invalidate a cursor.

Finite operations return JSON. Tool progress or a subscription can keep the same POST response open and emit JSON-RPC messages over SSE. Select a reader from the actual Content-Type; the helper above targets finite results.

GET does not open a separate MCP SSE channel, and EventSource cannot perform these POST calls. There is no session identified by Mcp-Session-Id or DELETE endpoint to close it. The kernel handles OPTIONS as HTTP introspection, rather than an MCP method. See Advanced workflows for progress and cancellation.

sequenceDiagram
    participant C as Client
    participant H as Orionis HTTP
    participant M as MCP dispatcher
    participant P as Component
    C->>H: POST + headers + JSON-RPC with _meta
    H->>H: Origin, API middleware, and HTTP validation
    H->>M: Method, parameters, and context
    M->>P: Construct, authorize, and invoke
    P-->>M: McpResponse or stream
    M-->>H: Validated envelope
    H-->>C: Finite JSON or SSE in the same response

The /mcp/demo and /mcp/support endpoints are registered in the multiple-server example. Each client must point to its server’s complete URL.

For a client or importer using the JSON mcpServers format, use:

{
"mcpServers": {
"DEMO_MCP": {
"url": "http://localhost:8000/mcp/demo",
"protocol": "stateless",
"supports_parallel_tool_calls": true
},
"SUPPORT_MCP": {
"url": "http://localhost:8000/mcp/support",
"protocol": "stateless",
"supports_parallel_tool_calls": true
}
}
}

Replace localhost:8000 with a host reachable from the agent process. DEMO_MCP and SUPPORT_MCP are client labels, rather than STDIO handles, route names, or required Server.name values.

For Hermes Agent, the native format is mcp_servers in ~/.hermes/config.yaml. Its equivalent is:

mcp_servers:
DEMO_MCP:
url: "http://localhost:8000/mcp/demo"
protocol: stateless
supports_parallel_tool_calls: true
"SUPPORT_MCP":
url: "http://localhost:8000/mcp/support"
protocol: stateless
supports_parallel_tool_calls: true

protocol: stateless tells Hermes to start with server/discover, appropriate for this revision; supports_parallel_tool_calls: true allows the client to overlap calls. It does not configure Orionis limits or make a tool safe for concurrent execution. These options are documented in the official Hermes reference; its official guide explains the mapping between root keys.

After changing configuration, use /reload-mcp inside Hermes to reload servers. The client must support MCP 2026-07-28; choosing stateless does not guarantee support for every advanced capability.

When the registration uses token authentication middleware, include credentials in client headers. This is one server entry:

{
"url": "https://api.example.com/mcp/demo",
"protocol": "stateless",
"supports_parallel_tool_calls": true,
"headers": {
"Authorization": "Bearer YOUR_TOKEN"
}
}

Use a valid token issued by your application. Endpoint authentication and tool authorization are separate controls; the latter can reject an operation even with a valid token. Server registration shows how to protect the endpoint.

SymptomCheck
HTTP 405Use POST to the registered endpoint; GET and DELETE do not execute MCP operations.
HTTP 406Explicitly include both Accept types without assigning q=0.
HTTP 415Send one Content-Type: application/json header and a JSON body.
HTTP 403 for an originA present Origin must exactly match allowed_origins. An absent origin is accepted; an empty list does not automatically allow same-origin requests.
Error -32020A mirrored header is missing, duplicated, or disagrees with the body. Check version, method, and name/URI.
Error -32022Unsupported revision: inspect error.data.supported.
Error -32601An unknown method, such as initialize or ping.
Error -32602Invalid parameters, cursor, or primitive; denied authorization can also use it with HTTP 403.
HTTP 413 or 503Request size or active capacity exceeded. An open stream retains its slot until closed.
Empty resources/listCheck templates in resources/templates/list and visibility hooks.

For limits and origins, see STDIO and configuration. For business errors and results, see MCP responses.