Journey. As a developer of an AI agent, I want my agent to discover and call
platform capabilities directly, so that I don't hand-write API integrations for
every service.
Every Inaya Labs service that is agent-facing exposes an MCP (Model Context Protocol) surface: a machine-readable tool catalog plus a calling convention. An MCP-capable agent (or a script acting like one) connects to one URL per service, asks what it can do, and calls it — no client SDK, no OpenAPI parsing at runtime. This guide uses the Memcache Service as the running example; the same pattern applies to every other service's /agent/v1/mcp/* endpoints.
Prerequisites
- The Memcache Service is reachable (local stack
:40011, or the shared dev
subdomain).
curl, or any HTTP client. Snippets also show Python and Node.- Sandbox exception. In the Sandbox environment (
INAYA_LABS_ENVIRONMENT=sandbox,
the Playground stack) MCP endpoints accept any or no credentials and run under a synthetic sandbox-user identity in the sandbox-tenant tenant — tenant-argument tools (e.g. add_cache_entry) must pass "tenant": "sandbox-tenant" there. Outside the Sandbox, present the same tenant key-pair headers (x-api-key / x-client-app-key) on MCP endpoints as you would on REST endpoints, and pass your own tenant id.
Concepts (only what this journey needs)
- One endpoint per service.
POST /agent/v1/mcp(streamable HTTP) carries
every JSON-RPC message and answers directly in its HTTP response — plain JSON when you send Accept: application/json, or a single SSE message event when you also accept text/event-stream. That one endpoint is the whole surface — the legacy SSE pair (GET /agent/v1/mcp/sse + POST /agent/v1/mcp/message) and the initialize handshake are not served (removed 2026-10-10).
- JSON-RPC envelope. Every message is `{"jsonrpc": "2.0", "id": <n>, "method":
<name>, "params": {...}}. Responses echo the id and carry either result or error. Keep id` unique per in-flight request.
- No session handshake (spec 2026-07-28). Each request declares its protocol
revision in params._meta["io.modelcontextprotocol/protocolVersion"] (or the MCP-Protocol-Version header, echoed on responses). Ask what the server speaks with server/discover; a revision the server does not support is rejected with UnsupportedProtocolVersionError (-32022) listing data.supported. The platform is latest-only: the legacy initialize handshake is not served.
- Three methods cover everything:
server/discover(versions + capabilities),
tools/list (tool catalog), tools/call (execution, with params.name and params.arguments).
- Tools mirror the REST API. Each MCP tool is a thin schema-guided wrapper over
the service's own operation — identical behavior, gentler calling convention. Tools that touch tenant data take the tenant identity as an explicit argument (e.g. tenant), not from a header.
Step 1 — Discover the server
Ask the service which protocol revisions and capabilities it supports. Modern agents do this once, cache the answer (ttlMs permits it), and skip any handshake:
curl -s -X POST "${BASE_URL}/agent/v1/mcp" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"jsonrpc": "2.0", "id": 1, "method": "server/discover", "params": {"_meta": {"io.modelcontextprotocol/protocolVersion": "2026-07-28", "io.modelcontextprotocol/clientInfo": {"name": "my-agent", "version": "1.0"}}}}'import json, os, urllib.request
req = urllib.request.Request(
f"{os.environ['BASE_URL']}/agent/v1/mcp",
data=json.dumps({
"jsonrpc": "2.0", "id": 1, "method": "server/discover",
"params": {"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": {"name": "my-agent", "version": "1.0"},
}},
}).encode(),
headers={"Content-Type": "application/json", "Accept": "application/json"},
)
print(json.dumps(json.load(urllib.request.urlopen(req)), indent=2))const res = await fetch(`${process.env.BASE_URL}/agent/v1/mcp`, {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify({
jsonrpc: "2.0", id: 1, method: "server/discover",
params: { _meta: {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": { name: "my-agent", version: "1.0" },
}},
}),
});
console.log(await res.json());Expected response — supported revisions, capabilities, and server identity:
{ "jsonrpc": "2.0", "id": 1, "result": { "resultType": "complete", "supportedVersions": ["2026-07-28"], "capabilities": { "tools": {}, "resources": {} }, "_meta": { "io.modelcontextprotocol/serverInfo": { "name": "memcache-service", "version": "0.1.0" } }, "ttlMs": 3600000, "cacheScope": "public" } }NOTE
Every result also carries a _meta["io.modelcontextprotocol/serverInfo"] identity stamp; it is omitted from the remaining examples for brevity.
Step 2 — Discover the tools
Ask the service what it can do. Agents should do this at session start rather than hard-coding tool names. Each request carries its protocol revision in params._meta:
curl -s -X POST "${BASE_URL}/agent/v1/mcp" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {"_meta": {"io.modelcontextprotocol/protocolVersion": "2026-07-28"}}}'import json, os, urllib.request
req = urllib.request.Request(
f"{os.environ['BASE_URL']}/agent/v1/mcp",
data=json.dumps({
"jsonrpc": "2.0", "id": 2, "method": "tools/list",
"params": {"_meta": {"io.modelcontextprotocol/protocolVersion": "2026-07-28"}},
}).encode(),
headers={"Content-Type": "application/json", "Accept": "application/json"},
)
print(json.dumps(json.load(urllib.request.urlopen(req)), indent=2))const res = await fetch(`${process.env.BASE_URL}/agent/v1/mcp`, {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify({
jsonrpc: "2.0", id: 2, method: "tools/list",
params: { _meta: { "io.modelcontextprotocol/protocolVersion": "2026-07-28" } },
}),
});
console.log(await res.json());Expected response — one entry per tool, each with a name, a plain-language description, and a JSON-schema argument list:
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"tools": [
{ "name": "add_cache_entry", "description": "Stores a value in the in-memory cache for a tenant. …", "inputSchema": { "type": "object", "properties": { "tenant": { "type": "string" }, "key": { "type": "string" }, "value": { "type": "string" }, "ttl_in_seconds": { "type": "integer" } }, "required": ["tenant", "key", "value", "ttl_in_seconds"] } },
{ "name": "get_cache_entry", "description": "Retrieves and decrypts a value …", "inputSchema": { "type": "object", "properties": { "tenant": { "type": "string" }, "key": { "type": "string" } }, "required": ["tenant", "key"] } },
{ "name": "delete_cache_entry", "description": "Permanently removes an entry …", "inputSchema": { "type": "object", "properties": { "tenant": { "type": "string" }, "key": { "type": "string" } }, "required": ["tenant", "key"] } }
]
}
}Step 3 — Call a tool
Store a value, then read it back. Note that tenant is an explicit argument and must match the caller's tenant claims — sandbox-tenant in the Sandbox environment, your own tenant id elsewhere:
curl -s -X POST "${BASE_URL}/agent/v1/mcp" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": {"name": "add_cache_entry", "arguments": {"tenant": "sandbox-tenant", "key": "agent-demo", "value": "hello from the agent", "ttl_in_seconds": 300}, "_meta": {"io.modelcontextprotocol/protocolVersion": "2026-07-28"}}}'import json, os, urllib.request
def call(base, msg_id, method, params):
params = {**params, "_meta": {"io.modelcontextprotocol/protocolVersion": "2026-07-28"}}
req = urllib.request.Request(
f"{base}/agent/v1/mcp",
data=json.dumps({"jsonrpc": "2.0", "id": msg_id,
"method": method, "params": params}).encode(),
headers={"Content-Type": "application/json", "Accept": "application/json"},
)
return json.load(urllib.request.urlopen(req))
base = os.environ["BASE_URL"]
print(json.dumps(call(base, 3, "tools/call", {
"name": "add_cache_entry",
"arguments": {"tenant": "sandbox-tenant", "key": "agent-demo",
"value": "hello from the agent", "ttl_in_seconds": 300},
}), indent=2))const call = async (id, method, params) => {
const res = await fetch(`${process.env.BASE_URL}/agent/v1/mcp`, {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify({ jsonrpc: "2.0", id, method,
params: { ...params, _meta: { "io.modelcontextprotocol/protocolVersion": "2026-07-28" } } }),
});
return res.json();
};
console.log(await call(3, "tools/call", {
name: "add_cache_entry",
arguments: { tenant: "sandbox-tenant", key: "agent-demo",
value: "hello from the agent", ttl_in_seconds: 300 },
}));Expected response — tool results arrive as content items; structured output is embedded JSON text. (isError appears only when the call fails, so its absence means success.)
{ "jsonrpc": "2.0", "id": 3, "result": { "content": [{ "type": "text", "text": "{\"stored\":true,\"tenant\":\"sandbox-tenant\",\"key\":\"agent-demo\"}" }] } }Read it back with get_cache_entry (same tenant and key):
curl -s -X POST "${BASE_URL}/agent/v1/mcp" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"jsonrpc": "2.0", "id": 4, "method": "tools/call", "params": {"name": "get_cache_entry", "arguments": {"tenant": "sandbox-tenant", "key": "agent-demo"}, "_meta": {"io.modelcontextprotocol/protocolVersion": "2026-07-28"}}}'Expected response:
{ "jsonrpc": "2.0", "id": 4, "result": { "content": [{ "type": "text", "text": "{\"found\":true,\"value\":\"hello from the agent\"}" }] } }PITFALL
A miss is not an error: get_cache_entry returns {"found": false} in a perfectly successful result for a missing or expired key. Agents should branch on found, not on the presence of isError. Also respect ttl_in_seconds — an entry that silently disappears was expired, not deleted.
TIP
Clean up after the demo: call delete_cache_entry with the same tenant and key when your agent is done — cache space is shared per tenant.
Full working example
A complete discover-then-call cycle — save as agent_probe.sh:
#!/usr/bin/env bash
set -euo pipefail
MCP="${BASE_URL}/agent/v1/mcp"
META='"_meta": {"io.modelcontextprotocol/protocolVersion": "2026-07-28"}'
HEADERS=(-H "Content-Type: application/json" -H "Accept: application/json")
echo "1. Discover:"
curl -sf -X POST "$MCP" "${HEADERS[@]}" \
-d "{\"jsonrpc\": \"2.0\", \"id\": 1, \"method\": \"server/discover\", \"params\": {$META}}" \
| python3 -c 'import json,sys; d=json.load(sys.stdin); print(" ", ", ".join(d["result"]["supportedVersions"]))'
echo "2. Tools:"
curl -sf -X POST "$MCP" "${HEADERS[@]}" \
-d "{\"jsonrpc\": \"2.0\", \"id\": 2, \"method\": \"tools/list\", \"params\": {$META}}" \
| python3 -c 'import json,sys; d=json.load(sys.stdin); print(" ", ", ".join(t["name"] for t in d["result"]["tools"]))'
echo "3. Store:"
curl -sf -X POST "$MCP" "${HEADERS[@]}" \
-d "{\"jsonrpc\": \"2.0\", \"id\": 3, \"method\": \"tools/call\", \"params\": {\"name\": \"add_cache_entry\", \"arguments\": {\"tenant\": \"sandbox-tenant\", \"key\": \"agent-demo\", \"value\": \"round-trip\", \"ttl_in_seconds\": 300}, $META}}" > /dev/null \
&& echo " ok"
echo "4. Retrieve:"
curl -sf -X POST "$MCP" "${HEADERS[@]}" \
-d "{\"jsonrpc\": \"2.0\", \"id\": 4, \"method\": \"tools/call\", \"params\": {\"name\": \"get_cache_entry\", \"arguments\": {\"tenant\": \"sandbox-tenant\", \"key\": \"agent-demo\"}, $META}}"
echo
echo "5. Cleanup:"
curl -sf -X POST "$MCP" "${HEADERS[@]}" \
-d "{\"jsonrpc\": \"2.0\", \"id\": 5, \"method\": \"tools/call\", \"params\": {\"name\": \"delete_cache_entry\", \"arguments\": {\"tenant\": \"sandbox-tenant\", \"key\": \"agent-demo\"}, $META}}" > /dev/null \
&& echo " ok"Try it live
The same Memcache Service backs the Playground → Infrastructure → Memcache console. Run the snippets above against the same base URL the Playground uses to watch both surfaces hit one service.
Copy agent prompt
You can use the Inaya Labs Memcache Service via MCP at ${BASE_URL}
(e.g. http://127.0.0.1:40011). Every request is a JSON-RPC POST to
/agent/v1/mcp with headers "Content-Type: application/json" and
"Accept: application/json", and carries
params._meta["io.modelcontextprotocol/protocolVersion"] = "2026-07-28".
Steps: (1) Call server/discover with id 1 and report the supported protocol
versions. (2) Call tools/list with id 2 and report the available tools. (3) Call
tools/call for add_cache_entry with {"tenant":"sandbox-tenant","key":"agent-prompt-demo","value":"...","ttl_in_seconds":300},
then get_cache_entry to read it back, then delete_cache_entry to clean up.
(4) Branch on the "found" field for reads, not on isError. Report each step's
outcome briefly.API reference
Next journeys
- Uploading files the platform way (G-04) — when your agent handles files, use the CDN's REST upload rather than MCP.
- Errors, retries & idempotency (G-03) — how MCP
errorresults and HTTP statuses map to retry decisions.