Skip to main content

FlowPOS MCP — AI Client Integration Guide (V2 · OAuth Token)

Connect AI assistants to FlowPOS using a Firebase-authenticated OAuth token. V2 is the recommended path for merchant-facing AI assistants, multi-business merchants, and intent tools that synthesize business data.

See also: V1 API Key guide for developer tooling, operator access, and simpler long-lived integrations.


What V2 gives you

  • Everything V1 provides, plus:
  • set_active_business — switch business context mid-session (only for merchants with multiple businesses)
  • Intent tools: summarize_day, summarize_period, get_inventory_health, get_client_health
  • OAuth sessions persist principal state in Redis (mcp:session:{sid}) so context switches survive across requests
  • Token scoped to the authenticated merchant's own businesses — no over-privileged keys

What V2 does NOT give you

  • Platform operator tools (list_tenants, set_active_tenant) — API key only
  • Infinite lifetime — tokens expire based on server configuration (MCP_TOKEN_TTL_SECONDS, default 86400 s / 24 h); you must refresh via POST /mcp/token
  • pos:intents scope by default — currently granted in the token exchange; confirm with your operator if intent tools are not visible

Get your OAuth token

If you have access to the FlowPOS PWA, you can generate a token without writing any code:

  1. Log in to the FlowPOS PWA
  2. Navigate to Settings → AI Assistant
  3. Click Generate Token
  4. Copy the token or use one of the pre-built configuration snippets for Claude Desktop, Claude Code, or Cursor

The page uses the same /mcp/token exchange described below. The expiry time is shown on the page after generation — return here to generate a new one when your AI assistant disconnects.

Manual exchange (programmatic)

Step 1 — Firebase login (frontend)

The merchant must authenticate with Firebase from your frontend application:

import { getAuth, signInWithEmailAndPassword } from "firebase/auth";

const auth = getAuth();
const cred = await signInWithEmailAndPassword(auth, email, password);
const firebaseIdToken = await cred.user.getIdToken();
// firebaseIdToken expires in 1 h — exchange it immediately

Step 2 — Exchange for a FlowPOS MCP token

curl -s -X POST https://api.flowpos.app/mcp/token \
-H "Content-Type: application/json" \
-d '{"firebaseIdToken": "<firebase-id-token>"}'

Response:

{
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expiresIn": 86400,
"role": "merchant",
"activeBusinessId": "biz-uuid-1",
"authorizedBusinessIds": ["biz-uuid-1", "biz-uuid-2"],
"scopes": ["pos:read", "pos:write", "reports:read", "psa:read", "pos:intents"]
}

Use the accessToken as a Bearer token on every subsequent MCP request:

Authorization: Bearer eyJhbGci...

Token refresh helpers

Python:

import requests

def get_flowpos_token(firebase_id_token: str) -> dict:
"""Exchange a Firebase ID token for a FlowPOS MCP access token."""
resp = requests.post(
"https://api.flowpos.app/mcp/token",
json={"firebaseIdToken": firebase_id_token},
timeout=10,
)
resp.raise_for_status()
return resp.json()
# { accessToken, expiresIn, role, activeBusinessId, authorizedBusinessIds, authorizedBusinesses, scopes }

TypeScript:

async function getFlowposToken(firebaseIdToken: string) {
const res = await fetch("https://api.flowpos.app/mcp/token", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ firebaseIdToken }),
});
if (!res.ok) throw new Error(`Token exchange failed: ${res.status}`);
return res.json() as Promise<{
accessToken: string;
expiresIn: number;
role: string;
activeBusinessId: string;
authorizedBusinessIds: string[];
authorizedBusinesses: Array<{ id: string; name: string }>;
scopes: string[];
}>;
}

Environments

Replace the MCP URL in any config block below to target a different environment.

EnvironmentMCP URL
Localhttp://localhost:4000/mcp
Staginghttps://api-staging.flowpos.app/mcp
Productionhttps://api.flowpos.app/mcp

Staging note: The api-staging.flowpos.app DNS alias may not yet be provisioned. If you get a connection error, use the direct Cloud Run URL: https://flowpos-backend-723334209984.us-central1.run.app/mcp

For local development, the backend must be running: pnpm --filter backend run start:dev


Claude

Claude has full native MCP support. Sessions, tool discovery, intent tools, and multi-turn tool use all work out of the box.

Claude Desktop (Mac / Windows)

Config file location:

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows: %APPDATA%\Claude\claude_desktop_config.json
{
"mcpServers": {
"flowpos": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://api.flowpos.app/mcp",
"--header",
"Authorization: Bearer eyJhbGci..."
]
}
}
}

mcp-remote bridges the HTTP/SSE transport to the stdio format Claude Desktop expects. Installed automatically by npx.

Save the file and restart Claude Desktop. With a V2 token, intent tools will appear in the tool picker alongside domain tools if the token includes pos:intents scope.

Test intent tools:

"Summarize today's business performance" "Which products need reordering right now?"

Token expiry: V2 tokens expire based on the server's MCP_TOKEN_TTL_SECONDS setting — check the expiry shown on the Settings → AI Assistant page after generation. When Claude Desktop shows FlowPOS tools as unavailable, update the accessToken value in the config file and restart Claude Desktop.

Multi-business: If authorizedBusinessIds contains more than one entry, set_active_business appears in the tool list. Tell Claude:

"Switch to my second business, then show me its inventory"


Claude Code (CLI)

# Add the server
claude mcp add flowpos \
--transport http \
--url https://api.flowpos.app/mcp \
--header "Authorization: Bearer eyJhbGci..."

Verify and use:

claude mcp list
claude "Summarize today's sales across all my locations"

Token refresh workflow (token expires — check expiry on the Settings → AI Assistant page):

# 1. Get a fresh Firebase token from your frontend, then exchange it:
NEW_TOKEN=$(curl -s -X POST https://api.flowpos.app/mcp/token \
-H "Content-Type: application/json" \
-d '{"firebaseIdToken": "<fresh-firebase-token>"}' \
| python3 -c "import sys,json; print(json.load(sys.stdin)['accessToken'])")

# 2. Remove the old server and add with the new token:
claude mcp remove flowpos
claude mcp add flowpos \
--transport http \
--url https://api.flowpos.app/mcp \
--header "Authorization: Bearer $NEW_TOKEN"

Claude Code (VS Code Extension)

The extension shares its MCP config with the CLI. Run the claude mcp add command above and the extension picks it up automatically.

Multi-business session in VS Code: Open Claude Code side panel and start with:

"I have two businesses. Switch to my second one and check inventory health."

Claude will call set_active_business automatically and scope all following tool calls to that business.


Claude.ai (web)

Claude.ai supports custom MCP servers via Settings → Integrations.

  1. Open claude.ai and go to Settings → Integrations
  2. Click Add MCP server
  3. Enter the FlowPOS MCP URL: https://api.flowpos.app/mcp
  4. Add a custom header: Authorization: Bearer eyJhbGci... (your V2 token)
  5. Save and start a new conversation — FlowPOS tools including intent tools will appear in the tool picker

Note: The Integrations panel is available on Pro and Team plans. If you do not see it, use Claude Desktop or Claude Code instead.

Token expiry: V2 tokens expire (default 24 h). When tools become unavailable, generate a new token from Settings → AI Assistant and update the header in Claude.ai Integrations.


Cursor

Cursor supports MCP through a per-project .cursor/mcp.json file. There is no global MCP config in Cursor — each project gets its own.

Step 1 — Get an OAuth token

Follow Step 1 and Step 2 above to obtain an accessToken from POST /mcp/token.

Step 2 — Create the config file

mkdir -p .cursor && touch .cursor/mcp.json

Step 3 — Add the FlowPOS server

{
"mcpServers": {
"flowpos": {
"url": "https://api.flowpos.app/mcp",
"headers": {
"Authorization": "Bearer eyJhbGci..."
}
}
}
}

Add .cursor/mcp.json to .gitignore — OAuth tokens are short-lived but should still be kept out of git.

Step 4 — Reload Cursor

Open the Command Palette (Cmd+Shift+P / Ctrl+Shift+P) and run Cursor: Reload Window.

Step 5 — Verify

  1. Open the Cursor chat panel (Cmd+L / Ctrl+L)
  2. Click the Tools icon — flowpos should appear. If pos:intents was included in your token, intent tools (summarize_day, etc.) will also be listed.
  3. Test with: "Summarize today's business performance"

Token expiry: V2 tokens expire based on the server's MCP_TOKEN_TTL_SECONDS setting. When Cursor shows the server as disconnected, update the Authorization value in .cursor/mcp.json with a fresh token and reload Cursor.

Multi-business: If your token has multiple authorizedBusinessIds, tell Cursor:

"Switch to my second business, then check its inventory health."


ChatGPT

The ChatGPT chat interface does not support MCP. Use the OpenAI API.

Option A — OpenAI Agents SDK (Python)

pip install openai-agents requests
import asyncio
import requests
from agents import Agent, Runner
from agents.mcp import MCPServerStreamableHTTP


def get_flowpos_token(firebase_id_token: str) -> str:
resp = requests.post(
"https://api.flowpos.app/mcp/token",
json={"firebaseIdToken": firebase_id_token},
timeout=10,
)
resp.raise_for_status()
return resp.json()["accessToken"]


async def main(access_token: str):
async with MCPServerStreamableHTTP(
url="https://api.flowpos.app/mcp",
headers={"Authorization": f"Bearer {access_token}"},
) as flowpos_server:
agent = Agent(
name="FlowPOS Merchant Assistant",
instructions=(
"You are a merchant business assistant with access to FlowPOS. "
"You can read sales data, inventory, orders, and PSA boards. "
"Use intent tools for synthesized summaries when available. "
"If the merchant has multiple businesses, call set_active_business "
"before querying data for a specific business."
),
mcp_servers=[flowpos_server],
)
result = await Runner.run(
agent,
"Summarize today's sales, then tell me which products need reordering.",
)
print(result.final_output)


# Get token from Firebase, then run
firebase_id_token = "<your-firebase-id-token>"
access_token = get_flowpos_token(firebase_id_token)
asyncio.run(main(access_token))

Multi-business example:

result = await Runner.run(
agent,
"I have two businesses. Switch to biz-uuid-2 and give me an inventory health report.",
)

The agent will call set_active_business with businessId: "biz-uuid-2" automatically before calling get_inventory_health.


Option B — OpenAI Responses API (direct HTTP)

import requests
from openai import OpenAI

def get_flowpos_token(firebase_id_token: str) -> str:
resp = requests.post(
"https://api.flowpos.app/mcp/token",
json={"firebaseIdToken": firebase_id_token},
timeout=10,
)
resp.raise_for_status()
return resp.json()["accessToken"]


access_token = get_flowpos_token("<firebase-id-token>")
client = OpenAI()

response = client.responses.create(
model="gpt-4o",
tools=[{
"type": "mcp",
"server_label": "flowpos",
"server_url": "https://api.flowpos.app/mcp",
"headers": {
"Authorization": f"Bearer {access_token}"
},
# Restrict to intent tools only:
# "allowed_tools": ["summarize_day", "summarize_period", "get_inventory_health"]
}],
input="Give me a summary of this week's performance and flag any inventory issues.",
)

print(response.output_text)

Requires gpt-4o or gpt-4o-mini. The chat.completions endpoint does not support MCP tool resources.


Gemini

Gemini has no native MCP support. Use a bridge adapter.

pip install langchain-mcp-adapters langchain-google-genai langgraph requests
import asyncio
import requests
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain_google_genai import ChatGoogleGenerativeAI
from langgraph.prebuilt import create_react_agent


def get_flowpos_token(firebase_id_token: str) -> str:
resp = requests.post(
"https://api.flowpos.app/mcp/token",
json={"firebaseIdToken": firebase_id_token},
timeout=10,
)
resp.raise_for_status()
return resp.json()["accessToken"]


async def main(access_token: str):
async with MultiServerMCPClient(
{
"flowpos": {
"url": "https://api.flowpos.app/mcp",
"transport": "streamable_http",
"headers": {"Authorization": f"Bearer {access_token}"},
}
}
) as client:
tools = await client.get_tools()

model = ChatGoogleGenerativeAI(
model="gemini-2.0-flash",
google_api_key="YOUR_GOOGLE_API_KEY",
)
agent = create_react_agent(model, tools)

result = await agent.ainvoke({
"messages": [{
"role": "user",
"content": (
"Check my inventory health and summarize what needs reordering. "
"Then give me a period summary for the last 30 days."
),
}]
})
print(result["messages"][-1].content)


access_token = get_flowpos_token("<firebase-id-token>")
asyncio.run(main(access_token))

Option B — Manual function bridge

import asyncio
import httpx
import requests
import google.generativeai as genai
from google.generativeai.types import FunctionDeclaration, Tool


def get_flowpos_token(firebase_id_token: str) -> str:
resp = requests.post(
"https://api.flowpos.app/mcp/token",
json={"firebaseIdToken": firebase_id_token},
timeout=10,
)
resp.raise_for_status()
return resp.json()["accessToken"]


MCP_URL = "https://api.flowpos.app/mcp"


async def init_mcp_session(access_token: str) -> tuple[httpx.AsyncClient, str]:
client = httpx.AsyncClient(
headers={"Authorization": f"Bearer {access_token}"}
)
resp = await client.post(MCP_URL, json={
"jsonrpc": "2.0", "id": 1, "method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": {"name": "gemini-bridge", "version": "1.0"},
},
})
return client, resp.headers["mcp-session-id"]


async def list_mcp_tools(client: httpx.AsyncClient, sid: str) -> list[dict]:
resp = await client.post(MCP_URL,
json={"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}},
headers={"mcp-session-id": sid},
)
return resp.json()["result"]["tools"]


async def call_mcp_tool(client: httpx.AsyncClient, sid: str, name: str, args: dict) -> str:
resp = await client.post(MCP_URL,
json={"jsonrpc": "2.0", "id": 3, "method": "tools/call",
"params": {"name": name, "arguments": args}},
headers={"mcp-session-id": sid},
)
return resp.json()["result"]["content"][0]["text"]


def to_gemini_tool(mcp_tool: dict) -> FunctionDeclaration:
schema = mcp_tool.get("inputSchema", {})
return FunctionDeclaration(
name=mcp_tool["name"],
description=mcp_tool["description"],
parameters=schema if schema.get("properties") else {"type": "object", "properties": {}},
)


async def main():
genai.configure(api_key="YOUR_GOOGLE_API_KEY")

access_token = get_flowpos_token("<firebase-id-token>")
client, sid = await init_mcp_session(access_token)
mcp_tools = await list_mcp_tools(client, sid)

model = genai.GenerativeModel(
"gemini-2.0-flash",
tools=[Tool(function_declarations=[to_gemini_tool(t) for t in mcp_tools])],
)
chat = model.start_chat()
response = chat.send_message(
"Summarize today's business performance and flag any inventory issues."
)

# Agentic loop — keep running until the model stops requesting tools
while response.candidates[0].content.parts[0].function_call.name:
fc = response.candidates[0].content.parts[0].function_call
result = await call_mcp_tool(client, sid, fc.name, dict(fc.args))
response = chat.send_message(
genai.protos.Part(
function_response=genai.protos.FunctionResponse(
name=fc.name,
response={"result": result},
)
)
)

print(response.text)
await client.aclose()


asyncio.run(main())

Troubleshooting

SymptomCauseFix
401 on POST /mcp/tokenFirebase token invalid or expiredFirebase ID tokens expire in 1 h — call user.getIdToken(true) to force-refresh
403 on POST /mcp/tokenUser has no active business membershipsCheck business_user table for is_active = true records for this Firebase UID
401 on POST /mcp, GET /mcp, or DELETE /mcpMCP access token missing, invalid, or expiredSend Authorization: Bearer <accessToken> on all MCP methods (including GET/DELETE), then refresh with POST /mcp/token/refresh or exchange a new Firebase token
Intent tools not in tools/listpos:intents not included in token scopesCurrently granted by default — confirm with operator if missing
set_active_business not in tools/listOnly one business in authorizedBusinessIdsTool only appears for merchants with 2+ businesses
Claude Desktop tools unavailableToken expiredUpdate accessToken in config file and restart Claude Desktop; check expiry on Settings → AI Assistant
Claude Code tools unavailableToken expiredclaude mcp remove flowpos, get new token, re-add
Claude.ai tools unavailableToken expiredGenerate a new token from Settings → AI Assistant, update the header in Claude.ai Settings → Integrations
Gemini bridge stops after one tool callMissing agentic loopImplement the while function_call loop shown in Option B above
Multi-business switch not reflected in dataset_active_business not called, or request hit a different instance without affinityCall set_active_business before read tools, and enable Cloud Run --session-affinity so requests stay on the same instance/session