# Raw HTTP (JSON-RPC)

> Call the FoodPerfect MCP server directly over HTTP with curl, TypeScript or Python — no MCP SDK needed.

The server speaks MCP's Streamable HTTP transport, **statelessly**: each `POST` is a complete JSON-RPC 2.0 request and gets a plain JSON response. You don't need an MCP SDK, a session or an `initialize` handshake to call tools, which makes it easy to use from scripts, backends and CI.

| | |
|---|---|
| Method and URL | `POST https://mcp.foodperfect.ai/mcp` |
| Headers | `Authorization: Bearer <key>`, `Content-Type: application/json`, `Accept: application/json, text/event-stream` |
| Write tools | also `Idempotency-Key: <unique per call>` |

## curl

List the tools:

```bash title="tools/list"
curl -sS https://mcp.foodperfect.ai/mcp \
  -H "Authorization: Bearer $FOODPERFECT_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
```

Call a read tool:

```bash title="tools/call: get_account"
curl -sS https://mcp.foodperfect.ai/mcp \
  -H "Authorization: Bearer $FOODPERFECT_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"get_account","arguments":{}}}'
```

Call a paid tool with an idempotency key:

```bash title="tools/call: generate_promo"
curl -sS https://mcp.foodperfect.ai/mcp \
  -H "Authorization: Bearer $FOODPERFECT_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"generate_promo","arguments":{"heading":"Lunch special","dishes":[{"name":"Lemongrass chicken","price":"$12"}],"ratio":"4:5"}}}'
```

A successful tool call returns the result both as `structuredContent` (an object) and as JSON text in `content`:

```json title="Response"
{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "content": [{ "type": "text", "text": "{\"plan\":\"growth\",\"totalBalanceUsd\":61.4, …}" }],
    "structuredContent": { "plan": "growth", "totalBalanceUsd": 61.4, "…": "…" }
  }
}
```

## A tiny client

A helper that adds a fresh idempotency key to each write call and turns tool errors into exceptions.

```ts tab="TypeScript"
const MCP_URL = 'https://mcp.foodperfect.ai/mcp';
const KEY = process.env.FOODPERFECT_API_KEY!;
let id = 0;

export async function callTool(name: string, args: Record<string, unknown> = {}, idempotencyKey = crypto.randomUUID()) {
  const res = await fetch(MCP_URL, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${KEY}`,
      'Content-Type': 'application/json',
      Accept: 'application/json, text/event-stream',
      'Idempotency-Key': idempotencyKey, // ignored by read tools
    },
    body: JSON.stringify({ jsonrpc: '2.0', id: ++id, method: 'tools/call', params: { name, arguments: args } }),
  });
  const body = await res.json();
  if (!res.ok) throw Object.assign(new Error(body.error?.message), body.error);          // 401, 403, 429
  if (body.error) throw new Error(body.error.message);                                  // JSON-RPC error
  const result = body.result;
  if (result.isError) throw Object.assign(new Error(result.structuredContent?.error?.message), result.structuredContent?.error);
  return result.structuredContent;
}

// const account = await callTool('get_account');
// const { generation } = await callTool('generate_photo', { dishes: [{ name: 'Margherita' }], styleId: 3 });
```

```python tab="Python"
import os, uuid, requests

MCP_URL = "https://mcp.foodperfect.ai/mcp"
KEY = os.environ["FOODPERFECT_API_KEY"]

class FoodPerfectError(Exception):
    def __init__(self, error: dict):
        super().__init__(error.get("message"))
        self.code, self.retryable, self.details = error.get("code"), error.get("retryable"), error.get("details")

def call_tool(name: str, args: dict | None = None, idempotency_key: str | None = None) -> dict:
    res = requests.post(MCP_URL, timeout=120, headers={
        "Authorization": f"Bearer {KEY}",
        "Content-Type": "application/json",
        "Accept": "application/json, text/event-stream",
        "Idempotency-Key": idempotency_key or str(uuid.uuid4()),  # ignored by read tools
    }, json={"jsonrpc": "2.0", "id": 1, "method": "tools/call",
             "params": {"name": name, "arguments": args or {}}})
    body = res.json()
    if not res.ok:
        raise FoodPerfectError(body["error"])            # 401, 403, 429
    if "error" in body:
        raise RuntimeError(body["error"]["message"])     # JSON-RPC error
    result = body["result"]
    if result.get("isError"):
        raise FoodPerfectError(result["structuredContent"]["error"])
    return result["structuredContent"]

# account = call_tool("get_account")
# photo = call_tool("generate_photo", {"dishes": [{"name": "Margherita"}], "styleId": 3})
```

To retry a write safely, pass the same `idempotencyKey` again.

## Using an MCP SDK

Official MCP SDKs work too: point a Streamable HTTP client transport at `https://mcp.foodperfect.ai/mcp` with the `Authorization` header. Most SDKs set headers per transport rather than per call, so for write tools either create a transport per call with its own `Idempotency-Key`, or use the helper above.

## Other endpoints

| Endpoint | Auth | Returns |
|---|---|---|
| `GET /health` | none | `{ ok, service, version }` |
| `GET /openapi.json` | none | A discovery document listing the tools and their schemas |

These live at the root of the MCP host, e.g. `https://mcp.foodperfect.ai/mcp` without `/mcp`.
