PHS Pallets MCP Gateway
Documentation
Everything you need to connect an AI assistant to the PHS Pallets catalog: endpoint details, authentication, and a complete reference for all 10 tools.
Introduction
The PHS Pallets MCP Gateway exposes live PHS production data — 100+ plastic pallet SKUs, real quotes with freight estimates, order history and shipment tracking — to any client that speaks the Model Context Protocol (ChatGPT, Claude, and any MCP-compatible agent).
| Property | Value |
|---|---|
| Endpoint | https://mcp.phsinc.com/api/mcp |
| Transport | Streamable HTTP (stateless, JSON responses) |
| Wire format | JSON-RPC 2.0 over HTTP POST |
| Tools | 10 (6 read-only, 4 non-destructive writes) |
| Server auth | Optional — anonymous OAuth 2.1 available for clients that require it |
| Data storage | None — every call proxies to the PHS production API over HTTPS |
Quick start
ChatGPT
Settings → Apps & Connectors → Create. Set Name to PHS Pallets, Server URL to the endpoint below, and Authentication to No Auth.
https://mcp.phsinc.com/api/mcp
Claude
Settings → Connectors → Add custom connector, then paste the same URL. Claude discovers the gateway's OAuth server automatically and connects in one click — no client ID or secret required.
cURL
curl -X POST https://mcp.phsinc.com/api/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'curl -X POST https://mcp.phsinc.com/api/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call",
"params":{"name":"popular_pallets","arguments":{"max_results":3}}}'Authentication
The gateway has two independent layers. Neither ever handles passwords in chat.
| Layer | Who it's for | How it works |
|---|---|---|
| Server access | MCP clients (ChatGPT, Claude) | Public — no credentials required. Clients that mandate OAuth (Claude) use the anonymous auto-approving OAuth 2.1 flow below. Issued Bearer tokens are accepted on every request. |
| Customer accounts | Returning PHS customers, inside the conversation | Passwordless one-time codes: request_login_code emails a 6-digit code, login_with_code exchanges it for a bearer token passed as a tool argument. |
OAuth 2.1 endpoints
A spec-compliant, auto-approving authorization server for MCP clients: dynamic client registration (RFC 7591), PKCE S256 (RFC 7636), authorization-server metadata (RFC 8414) and protected-resource metadata (RFC 9728). Codes and tokens are stateless HMAC-signed values.
| Endpoint | Method | Purpose |
|---|---|---|
| /.well-known/oauth-authorization-server | GET | Authorization-server discovery metadata. |
| /.well-known/oauth-protected-resource/api/mcp | GET | Protected-resource metadata for the MCP endpoint. |
| /api/oauth/register | POST (JSON) | Dynamic client registration — returns a public client_id (no secret). |
| /api/oauth/authorize | GET | Auto-approves and 302-redirects to redirect_uri with ?code=…&state=…. |
| /api/oauth/token | POST (form-encoded) | authorization_code (PKCE S256 verified) and refresh_token grants. |
curl -X POST https://mcp.phsinc.com/api/oauth/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=authorization_code" \ -d "code=<code from redirect>" \ -d "code_verifier=<PKCE verifier>" \ -d "redirect_uri=<your callback>" \ -d "client_id=<from /api/oauth/register>"
In-chat customer sign-in
The three-step passwordless flow assistants should follow for returning customers:
- Call
request_login_codewith the customer's account email. A single-use 6-digit code (10-minute expiry) is emailed. - Ask the customer for the code from their inbox — never ask for a password.
- Call
login_with_codeto exchange it for a bearer token, then pass that token tomy_orders,track_order,my_quotesandreorder.
Tools reference
Every tool publishes a typed outputSchema and MCP ToolAnnotations. Results arrive as structuredContent in the JSON-RPC response. Writes are strictly additive — nothing can be modified or deleted through this API.
search_pallets
READ-ONLY · IDEMPOTENTSearch the catalog by keyword and/or category. The gateway fetches the full product list from the PHS production API and filters in memory on name, description, pallet type and material.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| query | string | no | Keyword matched against name, description, pallet_type and material. Empty = match all. |
| category | string | no | Category filter, e.g. "rackable", "nestable", "stackable", "export", "hygienic". |
| max_results | integer | no | Maximum results to return. Default 8. |
Returns
| Field | Type | Description |
|---|---|---|
| results | PalletSummary[] | Matching pallets. Each includes: product_id, name, sku, pallet_type, material, length_in, width_in, height_in, weight_lb, capacity_static_lb, capacity_dynamic_lb, price_usd, quote_only, stock, availability, rackable, image, permalink. |
| count | integer | Number of results returned. |
Example
{"jsonrpc": "2.0", "id": 1, "method": "tools/call",
"params": {"name": "search_pallets",
"arguments": {"query": "rackable", "max_results": 3}}}get_pallet_details
READ-ONLY · IDEMPOTENTFull engineering spec sheet for a single SKU — dimensions, static/dynamic load, deck type, material, color, weight, price, availability and description.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| product_id | string | yes | PHS product id, e.g. "phs_15735". |
Returns
| Field | Type | Description |
|---|---|---|
| …PalletSummary | object | All summary fields (see search_pallets). |
| color | string | Pallet color. |
| deck_type | string | Deck construction, e.g. "Solid" or "Grid". |
| description | string | Full marketing/engineering description. |
Example
{"jsonrpc": "2.0", "id": 2, "method": "tools/call",
"params": {"name": "get_pallet_details",
"arguments": {"product_id": "phs_15735"}}}popular_pallets
READ-ONLY · IDEMPOTENTReal best sellers aggregated from PHS order history, plus a short buying guide mapping pallet types to use cases. The recommended starting point for new shoppers.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| max_results | integer | no | Maximum best sellers to return. Default 5. |
Returns
| Field | Type | Description |
|---|---|---|
| best_sellers | PalletSummary[] | Best-selling pallets with live specs and pricing. |
| buying_guide | string | Rackable = racking; stackable = floor stacking; nestable = return-freight savings; export = one-way, ISPM-15 exempt; hygienic = food/pharma washdown. |
Example
{"jsonrpc": "2.0", "id": 3, "method": "tools/call",
"params": {"name": "popular_pallets", "arguments": {"max_results": 5}}}request_quote
WRITE · NON-DESTRUCTIVEFiles a real quote request with the PHS sales team (same flow as the mobile app). PHS emails the customer a confirmation with a payment link — no payment ever happens in chat.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| contact_name | string | yes | Customer's full name. |
| contact_email | string | yes | Where the quote and payment link are emailed. |
| destination_zip | string | yes | US ZIP code for the freight estimate. |
| items | array | yes | [{product_id, quantity}] — one entry per pallet SKU. |
| company | string | no | Company name. |
| notes | string | no | Free-form notes for the sales team. |
Returns
| Field | Type | Description |
|---|---|---|
| quote_number | string | PHS quote reference. |
| status | string | Quote status, e.g. "pending". |
| subtotal_usd / freight_usd / total_usd | number | Itemized pricing with live freight estimate. |
| lead_time_days | integer | Estimated production/shipping lead time. |
| view_url | string | Browser link to review the quote. |
| message | string | Human-readable confirmation. |
Example
{"jsonrpc": "2.0", "id": 4, "method": "tools/call",
"params": {"name": "request_quote",
"arguments": {"contact_name": "Jane Doe",
"contact_email": "jane@acme.com",
"destination_zip": "75201",
"items": [{"product_id": "phs_15735", "quantity": 200}],
"company": "Acme Logistics"}}}request_login_code
WRITE · NON-DESTRUCTIVEStarts passwordless sign-in for a returning customer: emails a single-use 6-digit code (10-minute expiry). The response is identical whether or not the account exists, preventing enumeration. Assistants must never ask for passwords.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| string | yes | The customer's PHS Pallets account email. |
Returns
| Field | Type | Description |
|---|---|---|
| ok | boolean | Always true for accepted requests. |
| message | string | Generic confirmation that a code was emailed if the account exists. |
Example
{"jsonrpc": "2.0", "id": 5, "method": "tools/call",
"params": {"name": "request_login_code",
"arguments": {"email": "customer@example.com"}}}login_with_code
WRITE · NON-DESTRUCTIVECompletes passwordless sign-in: exchanges the emailed 6-digit code for a bearer token used by my_orders, track_order, my_quotes and reorder. Codes are single-use with attempt limits.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| string | yes | The account email the code was sent to. | |
| code | string | yes | The 6-digit code from the email. |
Returns
| Field | Type | Description |
|---|---|---|
| token | string | Bearer token — pass as the token argument to authenticated tools. |
| name | string | The customer's display name. |
Example
{"jsonrpc": "2.0", "id": 6, "method": "tools/call",
"params": {"name": "login_with_code",
"arguments": {"email": "customer@example.com", "code": "847291"}}}my_orders
READ-ONLY · IDEMPOTENTLists the signed-in customer's orders — status, totals, dates and line items.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| token | string | yes | Bearer token from login_with_code. |
Returns
| Field | Type | Description |
|---|---|---|
| orders | OrderSummary[] | order_id, order_number, status, total_usd, created_at, items. |
| count | integer | Number of orders. |
Example
{"jsonrpc": "2.0", "id": 7, "method": "tools/call",
"params": {"name": "my_orders", "arguments": {"token": "<bearer token>"}}}track_order
READ-ONLY · IDEMPOTENTLive shipment tracking for one of the customer's own orders: status, carrier, tracking number, ETA and the full event timeline (confirmed → processing → shipped → in transit → out for delivery → delivered).
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| order_id | string | yes | The order to track (from my_orders). |
| token | string | yes | Bearer token from login_with_code. |
Returns
| Field | Type | Description |
|---|---|---|
| order_id / status / carrier / tracking_number / eta | string | Current shipment state. |
| timeline | object[] | Chronological shipment events. |
Example
{"jsonrpc": "2.0", "id": 8, "method": "tools/call",
"params": {"name": "track_order",
"arguments": {"order_id": "<order id>", "token": "<bearer token>"}}}my_quotes
READ-ONLY · IDEMPOTENTLists the signed-in customer's quote pipeline with statuses, totals and line items.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| token | string | yes | Bearer token from login_with_code. |
Returns
| Field | Type | Description |
|---|---|---|
| quotes | QuoteSummary[] | quote_id, quote_number, status, total_usd, created_at, items. |
| count | integer | Number of quotes. |
Example
{"jsonrpc": "2.0", "id": 9, "method": "tools/call",
"params": {"name": "my_quotes", "arguments": {"token": "<bearer token>"}}}reorder
WRITE · NON-DESTRUCTIVEOne-step repeat purchase: creates a fresh quote from a past order (or the most recent order when order_id is omitted) at the customer's previous unit pricing with a fresh freight estimate, and emails a payment link. Strictly additive — the original order is never modified.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| token | string | yes | Bearer token from login_with_code. |
| order_id | string | no | Past order to reorder. Empty = most recent order. |
Returns
| Field | Type | Description |
|---|---|---|
| quote_number / status / items | mixed | The new quote and its line items. |
| subtotal_usd / freight_usd / total_usd | number | Re-priced totals with live freight. |
| lead_time_days / view_url | mixed | Lead time and browser link. |
| message | string | Confirms a payment-link email was sent — payment never happens in chat. |
Example
{"jsonrpc": "2.0", "id": 10, "method": "tools/call",
"params": {"name": "reorder", "arguments": {"token": "<bearer token>"}}}Protocol notes
- Stateless: no session or
initializehandshake is required beforetools/listortools/call. Every request is self-contained and horizontally scalable. - Headers: send
Content-Type: application/jsonandAccept: application/json, text/event-stream. - No redirects:
/api/mcpand/api/mcp/are equivalent — the gateway rewrites the path internally so clients never see a 307. - Structured output: tool results include both human-readable
contentand machine-readablestructuredContentvalidated against each tool'soutputSchema. - Annotations: read-only tools carry
readOnlyHint=true, idempotentHint=true; write tools carryreadOnlyHint=false, destructiveHint=false. All tools areopenWorldHint=false— the gateway only ever talks to the PHS first-party API. - Payments: never happen in chat. Quotes are paid via Stripe checkout through the emailed link or the PHS Pallets mobile app.
Error handling
Tool failures return a normal JSON-RPC result with isError: true and a plain-text explanation — upstream API problems are surfaced as PHS API error <status>: <detail>. Invalid or expired sign-in codes, bad tokens and PKCE failures all produce descriptive errors rather than 5xx responses.
{"jsonrpc": "2.0", "id": 5,
"result": {"isError": true,
"content": [{"type": "text",
"text": "PHS API error 401: Invalid or expired code"}]}}Resources
| Resource | URL |
|---|---|
| MCP endpoint | https://mcp.phsinc.com/api/mcp |
| ChatGPT app submission JSON (download) | https://mcp.phsinc.com/api/chatgpt-app-submission.json |
| App icon (256×256 PNG) | https://mcp.phsinc.com/icon-256.png |
| Plastic pallet catalog | https://www.phsinc.com/pallet/plastic-pallets/ |
| PHS Pallets on Google Play | https://play.google.com/store/apps/details?id=com.emergent.pallethub.wqcb7t |
| PHS Pallets on the App Store | https://apps.apple.com/us/app/phs-pallet/id6788104215 |
| Support | (800) 404-2311 · phsinc.com |
