# iJava MCP Server — Developer Docs

Version 1.0.0 · Updated 2026-10-02 · Endpoint: https://ijavacafe.com/api/mcp
Web version: https://ijavacafe.com/docs

## Overview

The iJava MCP server lets an AI assistant order real food from iJava Cafe in San Jose. It can read the cafes and the live menu, build a pickup order, hand the customer a secure payment link, and check the order afterwards.

It speaks the [Model Context Protocol](https://modelcontextprotocol.io), so Claude, Cursor and any other MCP client can use it with no SDK and no glue code.

|  |  |
| --- | --- |
| Endpoint | `https://ijavacafe.com/api/mcp` |
| Transport | Streamable HTTP, stateless. One JSON-RPC 2.0 message per `POST`, answered with JSON. |
| Protocol versions | `2025-06-18`, `2025-03-26`, `2024-11-05` |
| Authentication | None. See [Authentication and limits](https://ijavacafe.com/docs#auth). |
| Fulfilment | Pickup only, at Delmas (Downtown San Jose) and Willow Glen. |
| Payment | The customer pays on a Stripe page. The server never charges a card by itself. |

### What is available

| Piece | Status |
| --- | --- |
| MCP tools: `list_locations`, `search_menu`, `create_order`, `get_order_status` | **Live** |
| Customer accounts, favourites, `schedule_order`, `cancel_order` | Coming |
| Saved cards and spending rules ("Max $25", automatic orders) | Coming |
| REST API, API keys, webhooks, sandbox | Coming |

## Quickstart

Add the server to your client, then ask it something like *"What cold drinks does iJava Willow Glen have?"*

**Claude Code**

```bash
claude mcp add --transport http ijava https://ijavacafe.com/api/mcp
```

**Claude.ai / Desktop**

```text
Settings → Connectors → Add custom connector
Name: iJava
URL:  https://ijavacafe.com/api/mcp
```

**Cursor**

```json
{
  "mcpServers": {
    "ijava": {
      "url": "https://ijavacafe.com/api/mcp"
    }
  }
}
```

Add to `~/.cursor/mcp.json`, or `.cursor/mcp.json` in your project.

**Other clients**

```json
{
  "mcpServers": {
    "ijava": {
      "type": "http",
      "url": "https://ijavacafe.com/api/mcp"
    }
  }
}
```

Most clients take an `mcpServers` entry like this one. Pick the Streamable HTTP transport, not SSE.

**curl**

```bash
curl -s https://ijavacafe.com/api/mcp \
  -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"search_menu","arguments":{"location":"almaden","query":"latte"}}}'
```

No key and no sign-up. Every tool works the moment the client connects.

## How ordering works

1. **Pick a cafe.** Call `list_locations`. Use a cafe where `online_pickup_open` is `true`.
2. **Find the items.** Call `search_menu` to get each `item_id`, its sizes, and its required options.
3. **Confirm with the customer.** Read back the items and the price before you order.
4. **Create the order.** Call `create_order`. You get an `order_number` and a `payment_url`.
5. **Hand over the link.** The customer pays on Stripe's page. The order goes to the kitchen the moment they pay.
6. **Check on it.** Call `get_order_status` with the order number and the customer's email.

> **Note:** This is the **Ask every time** permission level: the agent prepares the order and the customer approves it by paying. Saved cards and rules-based or automatic ordering need customer accounts, which are coming.

When a cafe is not taking online pickup orders, `create_order` refuses with a link where the customer can order instead. Pass that link on.

## Authentication and limits

The server needs no key. The tools only read the public menu, or create an order the customer still has to pay for. Customer details are never returned, except to someone who already has both the order number and the email.

### Rate limits

| Limit | Per |
| --- | --- |
| 120 requests a minute | caller |
| 5 `create_order` calls a minute | caller |
| 20 `get_order_status` calls a minute | caller |
| 30 `create_order` calls a minute | all callers together |

Past a limit the server answers HTTP `429` with JSON-RPC error `-32000`. Wait a minute and try again. Request bodies over 64 KB get HTTP `413`.

## Tool: `list_locations`

_List iJava cafes. Read-only._

iJava cafe locations with address, phone, weekly hours, and whether online pickup orders are open right now.

### Arguments

None. Send an empty object, `{}`.

### Example

**Request**

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "list_locations",
    "arguments": {}
  }
}
```

**Result (the text of content[0], parsed)**

```json
[
  {
    "location": "delmas",
    "name": "iJava Cafe — Delmas",
    "address": "387 Delmas Ave, San Jose, CA 95126",
    "phone": "(408) 753-9732",
    "hours": {
      "sun": "06:30-14:00",
      "mon": "06:30-14:00",
      "…": "…"
    },
    "online_pickup_open": false,
    "order_elsewhere": "https://order.toasttab.com/online/ijava-cafe-delmas-387-delmas-avenue"
  },
  {
    "location": "almaden",
    "name": "iJava Cafe — Willow Glen",
    "address": "2306 Almaden Rd #150, San Jose, CA 95125",
    "phone": "(408) 979-0251",
    "hours": {
      "sun": "08:00-14:00",
      "mon": "08:00-14:00",
      "…": "…"
    },
    "online_pickup_open": true,
    "next_pickup": "ASAP"
  }
]
```

Hours are Pacific time, 24-hour clock. When `online_pickup_open` is `true` the result has `next_pickup` (`ASAP`, or the next opening such as `Saturday at 8:00 AM`). When it is `false` the result has `order_elsewhere`, a link the customer can order from instead.

## Tool: `search_menu`

_Search the menu. Read-only._

Search one cafe's orderable menu. Returns item_id, price, sizes and modifiers, which create_order needs. Prices are in USD before tax.

### Arguments

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `location` | `"delmas" \| "almaden"` | yes | delmas = Downtown San Jose, almaden = Willow Glen |
| `query` | `string` | no | Words to match in the item name, description or section. Omit to list the whole menu. (max 80 chars) |

### Example

**Request**

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "search_menu",
    "arguments": {
      "location": "almaden",
      "query": "cold brew"
    }
  }
}
```

**Result (the text of content[0], parsed)**

```json
{
  "location": "almaden",
  "count": 1,
  "items": [
    {
      "item_id": "cold-brew",
      "name": "Cold Brew",
      "section": "Coffee",
      "price": "$7.00",
      "modifiers": [
        {
          "name": "Milk",
          "choose": "exactly 1 (required)",
          "options": [
            "Regular milk",
            "Oat milk (+$0.75)",
            "Black (no milk)"
          ]
        },
        {
          "name": "Extras",
          "choose": "any (optional)",
          "options": [
            "Vanilla (+$0.75)",
            "Extra shot (+$0.75)"
          ]
        }
      ]
    }
  ]
}
```

Every word in `query` must match the item name, description, section or category. Omit `query` to get the whole orderable menu for that cafe (about 120 items).

Sized items return `sizes`, and you must send one of them as `size` in `create_order`. A modifier marked **required** must be sent. Send option names without the `(+$0.75)` suffix. Items the cafe has run out of today carry `"sold_out": true` and will be refused.

## Tool: `create_order`

_Create a pickup order. Creates an order._

Prices a pickup order and returns a secure Stripe payment link. Nothing is charged until the customer opens the link and pays; give the link to the customer. The kitchen gets the order once payment goes through. Confirm the items and total with the customer before calling this.

### Arguments

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `location` | `"delmas" \| "almaden"` | yes | delmas = Downtown San Jose, almaden = Willow Glen |
| `customer` | `object` | yes | The person picking up |
| `customer.name` | `string` | yes | Name on the pickup ticket (max 120 chars) |
| `customer.email` | `string` | yes | Gets the receipt; needed for get_order_status (email, max 320 chars) |
| `customer.phone` | `string` | yes | The cafe calls this number if there is a problem (max 40 chars) |
| `lines` | `object[]` | yes | One entry per menu item. At most 40 items in all (1–40 items) |
| `lines[].item_id` | `string` | yes | From search_menu |
| `lines[].quantity` | `integer` | yes | How many of this item (1–20) |
| `lines[].size` | `string` | no | Required when the item has sizes; one of its sizes |
| `lines[].modifiers` | `object[]` | no | Option names exactly as search_menu lists them, without the price suffix |
| `lines[].modifiers[].name` | `string` | yes |  |
| `lines[].modifiers[].options` | `string[]` | no |  |
| `lines[].special_instructions` | `string` | no | Note for this item (max 280 chars) |
| `special_instructions` | `string` | no | Note for the whole order (max 500 chars) |

### Example

**Request**

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "create_order",
    "arguments": {
      "location": "almaden",
      "customer": {
        "name": "Jordan Lee",
        "email": "jordan@example.com",
        "phone": "408-555-0100"
      },
      "lines": [
        {
          "item_id": "cold-brew",
          "quantity": 2,
          "modifiers": [
            {
              "name": "Milk",
              "options": [
                "Oat milk"
              ]
            }
          ]
        }
      ],
      "special_instructions": "Light ice"
    }
  }
}
```

**Result (the text of content[0], parsed)**

```json
{
  "order_number": "IJ-261002-4214BB",
  "payment_url": "https://checkout.stripe.com/c/pay/cs_live_…",
  "payment_link_expires_in_minutes": 30,
  "status": "pending_payment",
  "next_step": "Send payment_url to the customer. The order goes to the kitchen when they pay."
}
```

> **Important:** Confirm the items and the price with the customer before you call this tool. The customer pays on Stripe's page, so nothing is charged until they do, and an unpaid order never reaches the kitchen.

Prices, tax and the pickup time are set by the server. The payment page shows the final total with tax. The link stays valid for 30 minutes.

## Tool: `get_order_status`

_Check an order. Read-only._

Status, pickup time, items and total for an order. Needs the order number and the email it was placed with.

### Arguments

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `order_number` | `string` | yes | e.g. IJ-261002-A1B2C3, returned by create_order (max 40 chars) |
| `email` | `string` | yes | The email the order was placed with (email) |

### Example

**Request**

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "get_order_status",
    "arguments": {
      "order_number": "IJ-261002-4214BB",
      "email": "jordan@example.com"
    }
  }
}
```

**Result (the text of content[0], parsed)**

```json
{
  "order_number": "IJ-261002-4214BB",
  "status": "paid",
  "location": "almaden",
  "cafe": "iJava Cafe — Willow Glen",
  "address": "2306 Almaden Rd #150, San Jose, CA 95125",
  "pickup": "ASAP",
  "items": [
    {
      "name": "Cold Brew",
      "quantity": 2,
      "price": "$15.50"
    }
  ],
  "subtotal": "$15.50",
  "total_paid": "$16.95"
}
```

| status | Meaning |
| --- | --- |
| `pending_payment` | Link created, the customer has not paid yet. |
| `paid` | Paid. The order is with the cafe. `refunded` appears if staff refunded any of it. |
| `payment_failed` | Checkout could not start or did not complete. |
| `canceled` | Staff canceled the order. |

A wrong email and an unknown order number get the same answer, `No order matches that number and email.`

## Errors

There are two kinds. A **tool error** means the tool ran and said no: bad arguments, a sold-out item, a closed cafe. It comes back as a normal result with `isError: true` and a sentence your model can read and act on.

**Tool error**

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "Complete Milk for Cold Brew."
      }
    ],
    "isError": true
  }
}
```

A **protocol error** means the request itself was wrong. It comes back as a JSON-RPC `error`.

| Code | HTTP | Meaning |
| --- | --- | --- |
| `-32700` | 400 | The body is not valid JSON. |
| `-32600` | 400 / 413 | Not a single JSON-RPC 2.0 message, or the body is over 64 KB. |
| `-32601` | 200 | Unknown method. The server supports `initialize`, `ping`, `tools/list` and `tools/call`. |
| `-32602` | 200 | Unknown tool name. |
| `-32000` | 429 | Rate limited. |

## Raw protocol

You only need this to call the server without an MCP client. Every request is a `POST` to the endpoint with one JSON-RPC message. There are no sessions: you can skip `initialize` and call `tools/call` directly.

**List the tools**

```bash
curl -s https://ijavacafe.com/api/mcp \
  -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
```

**Call a tool**

```bash
curl -s https://ijavacafe.com/api/mcp \
  -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"list_locations","arguments":{}}}'
```

The result of `tools/call` is `{ content: [{ type: "text", text }] }`, where `text` is JSON. `tools/list` returns each tool's full JSON Schema as `inputSchema`. Notifications get HTTP `202` with no body. `GET` and `DELETE` get `405`, because there is no event stream and no session to end. CORS is open, so a browser page can call the server directly.

## Changelog

| Date | Version | Change |
| --- | --- | --- |
| 2026-10-02 | 1.0.0 | First release: `list_locations`, `search_menu`, `create_order`, `get_order_status`. |
