Home Playground Docs hub

Guide G-02 · Platform-wide

Calling services from an AI agent (MCP)

Verified against sandbox · 2026-09-23 · memcache-service 0.1.0 (workspace HEAD)
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

subdomain).

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)

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).

<name>, "params": {...}}. Responses echo the id and carry either result or error. Keep id` unique per in-flight request.

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.

tools/list (tool catalog), tools/call (execution, with params.name and params.arguments).

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:

bash
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"}}}}'
python
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))
javascript
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:

json
{ "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:

bash
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"}}}'
python
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))
javascript
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:

json
{
  "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:

bash
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"}}}'
python
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))
javascript
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.)

json
{ "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):

bash
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:

json
{ "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:

bash
#!/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

Agent prompt — copy and hand to your agent
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