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 viaPOST /mcp/token pos:intentsscope by default — currently granted in the token exchange; confirm with your operator if intent tools are not visible
Get your OAuth token
From the FlowPOS App (recommended)
If you have access to the FlowPOS PWA, you can generate a token without writing any code:
- Log in to the FlowPOS PWA
- Navigate to Settings → AI Assistant
- Click Generate Token
- 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.
| Environment | MCP URL |
|---|---|
| Local | http://localhost:4000/mcp |
| Staging | https://api-staging.flowpos.app/mcp |
| Production | https://api.flowpos.app/mcp |
Staging note: The
api-staging.flowpos.appDNS 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/mcpFor 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-remotebridges the HTTP/SSE transport to the stdio format Claude Desktop expects. Installed automatically bynpx.
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.
- Open claude.ai and go to Settings → Integrations
- Click Add MCP server
- Enter the FlowPOS MCP URL:
https://api.flowpos.app/mcp - Add a custom header:
Authorization: Bearer eyJhbGci...(your V2 token) - 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.jsonto.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
- Open the Cursor chat panel (
Cmd+L/Ctrl+L) - Click the Tools icon — flowpos should appear. If
pos:intentswas included in your token, intent tools (summarize_day, etc.) will also be listed. - 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-4oorgpt-4o-mini. Thechat.completionsendpoint does not support MCP tool resources.
Gemini
Gemini has no native MCP support. Use a bridge adapter.
Option A — LangChain MCP Adapter (recommended)
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
| Symptom | Cause | Fix |
|---|---|---|
401 on POST /mcp/token | Firebase token invalid or expired | Firebase ID tokens expire in 1 h — call user.getIdToken(true) to force-refresh |
403 on POST /mcp/token | User has no active business memberships | Check business_user table for is_active = true records for this Firebase UID |
401 on POST /mcp, GET /mcp, or DELETE /mcp | MCP access token missing, invalid, or expired | Send 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/list | pos:intents not included in token scopes | Currently granted by default — confirm with operator if missing |
set_active_business not in tools/list | Only one business in authorizedBusinessIds | Tool only appears for merchants with 2+ businesses |
| Claude Desktop tools unavailable | Token expired | Update accessToken in config file and restart Claude Desktop; check expiry on Settings → AI Assistant |
| Claude Code tools unavailable | Token expired | claude mcp remove flowpos, get new token, re-add |
| Claude.ai tools unavailable | Token expired | Generate a new token from Settings → AI Assistant, update the header in Claude.ai Settings → Integrations |
| Gemini bridge stops after one tool call | Missing agentic loop | Implement the while function_call loop shown in Option B above |
| Multi-business switch not reflected in data | set_active_business not called, or request hit a different instance without affinity | Call set_active_business before read tools, and enable Cloud Run --session-affinity so requests stay on the same instance/session |