Connect an AI agent to ReceiptRoller over MCP
In this quickstart you connect an AI agent to the ReceiptRoller MCP server so it can work with sales, products, bookings and other data from a conversation. The first part connects Claude as a connector. The second part is for developers building their own MCP client: you fetch the discovery documents, register a client dynamically, get a token with the authorization code flow and PKCE, call initialize, tools/list and tools/call with curl, and then automate it with a Node.js script.
What you need first
- A ReceiptRoller account that belongs to a business account. The tools return that business account's real data; there is no sandbox yet.
- You do not register an app in the developer portal for MCP. The client registers itself through dynamic client registration.
- For the second part:
curl,openssland Node.js 20 or later.
Every tool calls the ReceiptRoller API with the connected user's own access token. The API's scope checks and role checks therefore apply unchanged: an operation that needs a scope the user did not approve, or that the user's role does not allow (for example the sales figures of a store they cannot see sales for), is refused.
Connection details
| Item | Value |
|---|---|
| MCP endpoint | https://mcp.receiptroller.io/mcp (the /mcp path is required; a POST to the root returns 405) |
| Transport | HTTP POST, JSON-RPC 2.0, protocol version 2025-03-26 |
| Authorization endpoint | https://receiptroller.io/oauth/authorize |
| Token endpoint | https://receiptroller.io/api/v1/auth/token |
| Client registration | POST https://mcp.receiptroller.io/register |
Scopes requested
An MCP client can request these 14 scopes. The consent screen lists the ones the client asked for.
| Scope | Main tools |
|---|---|
sales.read | sales_get_kpis and the other sales_* tools |
store.products.read / store.products.write | pim_list_products, pim_get_product / pim_create_product, pim_update_product |
store.orders.read / store.orders.write | oms_* |
store.inventory.read / store.inventory.write | wms_* |
store.reservations.read, store.memberships.read, store.passes.read | reservation_*, membership_list_members, pass_list_holders (read only) |
sns.content.read, sns.content.write, sns.audience.read, analytics.read | sns_* |
For what each scope allows, see OAuth Scopes Reference and Availability.
Part 1: Connect Claude
- In Claude (claude.ai or Claude Desktop), add a custom connector in the connector settings.
- Enter
https://mcp.receiptroller.io/mcpas the URL and save. - When you connect, a browser opens the ReceiptRoller sign-in page. Sign in.
- Review the consent screen and choose Approve. Back in Claude, the connector shows as connected.
The consent screen shows the client's name, a line saying it wants to access your ReceiptRoller account and that you choose the business account on each request, and a "Permissions requested" list with one checkbox per scope, grouped by category. Every box starts checked; any you clear is not granted, and approving with all of them cleared counts as a refusal. You do not pick a business account on this screen. Each tool call names it with organizationId.
Try it
| Ask | Tools called |
|---|---|
| "Show this month's KPIs for my whole business account, compared with last month." | list_my_organizations then sales_get_kpis |
| "List the products in our product master whose name or SKU contains 'coffee'." | pim_list_products (search) |
| "List tomorrow's bookings at the Shibuya store." | list_my_stores then reservation_list (from / to) |
Sales tools need an administrator role in the business account for account-wide figures, or a role that can see sales at the store for store figures. Without it the tool returns an error starting with Upstream 403.
Part 2: Connect your own MCP client
From here you run, one step at a time with curl, what Claude does behind the scenes.
Step 1: Fetch the discovery documents
curl https://mcp.receiptroller.io/.well-known/oauth-protected-resource
{
"resource": "https://mcp.receiptroller.io/mcp",
"authorization_servers": ["https://mcp.receiptroller.io"],
"bearer_methods_supported": ["header"],
"scopes_supported": ["sns.content.read", "sns.content.write", "...", "store.passes.read"]
}
The same document is also served at /.well-known/oauth-protected-resource/mcp. Next, fetch the authorization server metadata.
curl https://mcp.receiptroller.io/.well-known/oauth-authorization-server
{
"issuer": "https://mcp.receiptroller.io",
"authorization_endpoint": "https://receiptroller.io/oauth/authorize",
"token_endpoint": "https://receiptroller.io/api/v1/auth/token",
"registration_endpoint": "https://mcp.receiptroller.io/register",
"scopes_supported": ["sns.content.read", "...", "store.passes.read"],
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"token_endpoint_auth_methods_supported": ["client_secret_post", "none"],
"code_challenge_methods_supported": ["S256", "plain"]
}
The authorization and token endpoints are on receiptroller.io, not on mcp.receiptroller.io.
Step 2: Register a client
curl -X POST https://mcp.receiptroller.io/register \
-H "Content-Type: application/json" \
-d '{
"client_name": "My MCP Client",
"redirect_uris": ["http://localhost:8765/callback"]
}'
The response is 201:
{
"client_id": "mcp_xxxxxxxxxxxxxxxxxxxx",
"client_secret": "{client_secret}",
"client_id_issued_at": 1793000000,
"client_secret_expires_at": 0,
"redirect_uris": ["http://localhost:8765/callback"],
"client_name": "My MCP Client",
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "client_secret_post"
}
- The
client_secretis shown only in this response. Store it. - Only the first entry in
redirect_urisis stored. Use exactly that value in the authorization request and the token exchange. client_nameappears on the consent screen. If you omit it, it isUnnamed MCP client.- An empty
redirect_urisreturns 400invalid_redirect_uri.
Step 3: Create the PKCE values
code_verifier=$(openssl rand -base64 48 | tr -d '=+/\n' | cut -c1-64)
code_challenge=$(printf '%s' "$code_verifier" | openssl dgst -sha256 -binary | openssl base64 | tr '+/' '-_' | tr -d '=\n')
echo "$code_verifier"; echo "$code_challenge"
Step 4: Authorize in the browser
Open this URL in a browser (line breaks are for readability; use one line and URL-encode the values).
https://receiptroller.io/oauth/authorize
?response_type=code
&client_id={client_id}
&redirect_uri=http%3A%2F%2Flocalhost%3A8765%2Fcallback
&scope=sales.read%20store.products.read%20store.reservations.read
&state={state}
&code_challenge={code_challenge}
&code_challenge_method=S256
After you sign in and choose Approve, the browser is redirected to http://localhost:8765/callback?code={code}&state={state}. If you choose Deny, the redirect carries ?error=access_denied. The authorization code is valid for 10 minutes and can be used once.
| Error (400) | Cause and fix |
|---|---|
unsupported_response_type | Send response_type=code. |
invalid_client | The client_id is not registered. Check the value from step 2. |
invalid_redirect_uri | It does not exactly match the first registered redirect_uris entry. |
Step 5: Exchange the code for a token
curl -X POST https://receiptroller.io/api/v1/auth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d grant_type=authorization_code \
-d code={code} \
-d redirect_uri=http://localhost:8765/callback \
-d client_id={client_id} \
-d client_secret={client_secret} \
-d code_verifier={code_verifier}
{
"access_token": "{access_token}",
"refresh_token": "{refresh_token}",
"token_type": "Bearer",
"expires_in": 28800,
"scope": "sales.read store.products.read store.reservations.read"
}
The response also carries camelCase duplicates such as accessToken for older clients. The access token lasts 8 hours. Although the metadata lists none, a dynamically registered client is a confidential client, so always send client_secret.
| Error (401) | Cause and fix |
|---|---|
invalid_client | The client_id or client_secret is wrong. |
invalid_grant | The code is expired or already used, the redirect_uri differs from the authorization request, code_verifier is missing, or PKCE verification failed. Read error_description and start again from step 4. |
This endpoint returns 401, not 400, for invalid_grant. When the token expires, refresh it with grant_type=refresh_token, refresh_token, client_id and client_secret. See Your first API call: from app registration to listing transactions.
Step 6: initialize
curl -X POST https://mcp.receiptroller.io/mcp \
-H "Authorization: Bearer {access_token}" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}'
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2025-03-26",
"capabilities": { "tools": {} },
"serverInfo": { "name": "Receipt Roller MCP", "version": "0.1.0" }
}
}
Every response is a single JSON body. There is no session header and no streaming; each request stands alone. The server handles initialize, tools/list, tools/call and ping. Anything else, including notifications/initialized, gets a -32601 error, which you can ignore.
Step 7: tools/list
curl -X POST https://mcp.receiptroller.io/mcp \
-H "Authorization: Bearer {access_token}" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"tools": [
{
"name": "list_my_organizations",
"description": "List every ReceiptRoller organization the authorized user belongs to. ...",
"inputSchema": { "type": "object", "properties": {}, "additionalProperties": false },
"annotations": { "readOnlyHint": true, "idempotentHint": true }
},
{ "name": "list_my_stores", "...": "..." },
{ "name": "sales_get_kpis", "...": "..." }
]
}
}
The list contains every tool whatever scopes the token holds. A tool whose scope is missing fails when you call it.
Step 8: tools/call
curl -X POST https://mcp.receiptroller.io/mcp \
-H "Authorization: Bearer {access_token}" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"list_my_organizations","arguments":{}}}'
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"content": [ { "type": "text", "text": "1 organization(s)" } ],
"structuredContent": {
"organizations": [
{ "id": "{organizationId}", "name": "Sample Shop", "role": "SuperUser", "roles": ["SuperUser"] }
],
"page": 1,
"pageSize": 1000,
"totalCount": 1,
"totalPages": 1
}
}
}
The data is in structuredContent. Pass the id as organizationId to the other tools. For example, to get KPIs:
curl -X POST https://mcp.receiptroller.io/mcp \
-H "Authorization: Bearer {access_token}" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"sales_get_kpis","arguments":{"organizationId":"{organizationId}"}}}'
Step 9: Call it from Node.js
Given an access token in an environment variable, this script prints the tool list and calls list_my_organizations. It needs no extra packages.
// mcp-check.mjs — node mcp-check.mjs
const MCP_URL = "https://mcp.receiptroller.io/mcp";
const accessToken = process.env.RR_ACCESS_TOKEN;
if (!accessToken) {
console.error("Set RR_ACCESS_TOKEN first.");
process.exit(1);
}
let nextId = 1;
async function rpc(method, params = {}) {
const response = await fetch(MCP_URL, {
method: "POST",
headers: {
"Authorization": `Bearer ${accessToken}`,
"Content-Type": "application/json"
},
body: JSON.stringify({ jsonrpc: "2.0", id: nextId++, method, params })
});
if (response.status === 401) {
throw new Error(`401: ${response.headers.get("www-authenticate")}`);
}
const body = await response.json();
if (body.error) {
throw new Error(`JSON-RPC ${body.error.code}: ${body.error.message}`);
}
return body.result;
}
const { tools } = await rpc("tools/list");
console.log(`${tools.length} tools:`);
for (const tool of tools) console.log(` ${tool.name}`);
const result = await rpc("tools/call", { name: "list_my_organizations", arguments: {} });
if (result.isError) {
console.error("Tool error:", result.content[0].text);
process.exit(1);
}
for (const org of result.structuredContent.organizations) {
console.log(`${org.id} ${org.name} (${org.roles.join(", ")})`);
}
RR_ACCESS_TOKEN={access_token} node mcp-check.mjs
Common errors
401 and WWW-Authenticate
Without an Authorization header the server returns 401 with no body and this header:
WWW-Authenticate: Bearer resource_metadata="https://mcp.receiptroller.io/.well-known/oauth-protected-resource/mcp"
An MCP client follows that URL to the discovery documents and starts authorization. When the token is expired, revoked or unknown, the header also carries error="invalid_token". Refresh the token or authorize again.
JSON-RPC errors and tool errors
| What you get | Meaning and fix |
|---|---|
HTTP 400 with error.code -32700 | The body is not valid JSON. |
error.code -32601 | The server does not handle that method. |
result.isError true, organizationId is required... | Call list_my_organizations first and pass the id. Booking tools also need storeId (from list_my_stores). |
result.isError true, Upstream 403 | The token lacks the scope, or the user is not allowed that operation in that business account or store. See 403/401 is returned (permissions and scopes). |
result.isError true, Unknown tool: ... | The tool name is wrong. Use a name from tools/list. |
Next steps
- MCP for developers (connection details, scopes and tools)
- AI agent integration (for store owners)
- OAuth Scopes Reference and Availability
- Your first API call: from app registration to listing transactions
- Listing business accounts, stores, and POS terminals
- Using the Sales API
- 403/401 is returned (permissions and scopes)