BSN Inventory MCP
by aapatil67
README.md
# BSN Inventory MCP
Streamable HTTP MCP server for BSN SKU/material identification and inventory availability.
## Setup
```bash
npm install
cp .env.example .env
```
Edit `.env` before running. Keep secrets in `.env` or the parent shell, not in source files.
Required to start: set at least one inbound MCP bearer key:
```text
BSN_MCP_API_KEY=
BSN_MCP_API_KEY_1=
BSN_MCP_API_KEY_2=
```
Clients must send:
```text
Authorization: Bearer <local_mcp_api_key>
```
When the server is hosted behind Amazon Bedrock AgentCore Runtime with `AWS_IAM`,
SigV4 uses the `Authorization` header. In that case, configure a custom key
header and allowlist that header in the AgentCore runtime:
```text
BSN_MCP_API_KEY_HEADER=X-Api-Key
```
`BSN_MCP_API_KEY` is kept as a legacy slot-1 alias. For key rotation, prefer numbered slots. The server accepts any configured value from `BSN_MCP_API_KEY`, `BSN_MCP_API_KEY_1`, or `BSN_MCP_API_KEY_2`.
Inventory lookup defaults to PROD. These values are required only when calling `getInventoryAvailability`:
```text
BSN_INVENTORY_API_ENV=prod
BSN_API_BASE_URL_QA=https://api-qa.bsnsports.com:443
BSN_API_BASE_URL_PROD=https://api.bsnsports.com:443
BSN_INVENTORY_API_KEY_QA=
BSN_INVENTORY_API_KEY_PROD=
BSN_INVENTORY_API_KEY=
BSN_INVENTORY_RESPONSE_PAYLOAD_FORMAT=pretty
```
`BSN_INVENTORY_RESPONSE_PAYLOAD_FORMAT` supports `pretty` and `json`. Use `pretty` by default because the server normalizes that payload into a complete color/size inventory matrix.
SKU/material identification uses local brand SKU catalogs in `resources/skus/*.jsonl` first. OpenAI file search is the fallback path when the local catalog has weak, ambiguous, or unavailable results. Set at least one OpenAI key before calling `getVectorStoreFiles` or before relying on vector fallback from `identifySkuMaterial`:
```text
OPENAI_API_KEY=
OPENAI_API_KEY_1=
OPENAI_API_KEY_2=
OPENAI_SKU_VECTOR_STORE_IDS=
OPENAI_SKU_BRAND_VECTOR_STORE_IDS=
OPENAI_SKU_SEARCH_MODEL=gpt-5.5
OPENAI_SKU_SEARCH_MAX_OUTPUT_TOKENS=900
```
`OPENAI_SKU_SEARCH_MODEL` is optional and defaults to `gpt-5.5`. Set it in `.env` to use a different model.
`OPENAI_API_KEY` is kept as a legacy slot-1 alias. For rotation, prefer `OPENAI_API_KEY_1` and `OPENAI_API_KEY_2`. The server tries configured OpenAI keys in order and only falls back to the next configured key when OpenAI rejects the current key for authentication or authorization.
`OPENAI_SKU_VECTOR_STORE_IDS` is optional. When set, it scopes broad fallback SKU search and `getVectorStoreFiles` to those comma-separated vector stores. When unset, vector fallback from `identifySkuMaterial` discovers accessible completed OpenAI vector stores at runtime, and `getVectorStoreFiles` lists accessible stores from OpenAI. `identifySkuMaterial` also accepts per-call `vectorStoreIds` when a client needs to search a specific store such as SanMar.
`OPENAI_SKU_BRAND_VECTOR_STORE_IDS` is optional. When set, vector fallback from `identifySkuMaterial` routes a provided `brand` to that brand's vector store before broad env or discovery search. Use comma-separated `brand=vectorStoreId` entries, such as `nike=vs_...,adidas=vs_...`; use `|` inside one entry if a brand needs multiple stores. Fallback vector-store precedence is explicit `vectorStoreIds`, then brand route, then `OPENAI_SKU_VECTOR_STORE_IDS`, then OpenAI discovery.
The local SanMar JSONL catalog is searched as the distributor/multi-brand fallback. Include the SanMar `sanmar_skus` vector store as the OpenAI fallback for SanMar-sourced and `SM*` catalog families before using BSN Shop or web search as a last resort; pass its current vector store ID through `identifySkuMaterial.vectorStoreIds` when it was not already searched.
## Key Rotation
Use numbered key slots to rotate without interrupting users:
1. Put the current key in `BSN_MCP_API_KEY_1` or `OPENAI_API_KEY_1`.
2. Put the new key in `BSN_MCP_API_KEY_2` or `OPENAI_API_KEY_2`.
3. Update clients or the OpenAI project/provider side to use the new key.
4. Remove the old key after traffic has moved.
5. Optionally move the new key back to slot 1 before the next rotation.
The unnumbered `BSN_MCP_API_KEY` and `OPENAI_API_KEY` variables continue to work for existing installs. Avoid keeping different values in both legacy and numbered slot 1 longer than needed; leave only the active numbered slots after rotation.
## Run
```bash
npm run dev
```
Or run compiled output:
```bash
npm run build
npm run start
```
Default MCP endpoint:
```text
http://127.0.0.1:3100/mcp
```
For client-specific connection examples and hosted-client notes, see [MCP_CLIENT_CONNECTIONS.md](MCP_CLIENT_CONNECTIONS.md).
## Source Layout
- `src/main.ts` is the process entrypoint and owns startup validation plus `listen()`.
- `src/http/app.ts` owns the Streamable HTTP endpoint, bearer auth boundary, and per-request transport lifecycle.
- `src/mcp/server.ts` creates the `McpServer` and registers resources, prompts, and config metadata.
- `src/tools/registerInventoryTools.ts` registers the public MCP tools.
- `src/inventory/`, `src/openai/`, `src/resources/`, and `src/prompts/` contain the domain-specific helpers used by those tools.
- `src/index.ts` is import-safe and only re-exports modules for tests or local consumers.
## Architecture
```mermaid
flowchart TB
Client["MCP Client"] -->|"POST /mcp<br/>Authorization: Bearer key"| App
subgraph Http["HTTP/Auth Boundary"]
App["Express MCP App<br/>src/http/app.ts"]
Auth["Bearer Auth<br/>src/auth/mcpAuth.ts"]
Reject["Unauthorized response<br/>no transport allocated"]
App --> Auth
Auth -->|"authorized"| Transport["Stateless Streamable HTTP Transport<br/>per request"]
Auth -.->|"missing/invalid bearer key"| Reject
end
subgraph Runtime["Per-request MCP Runtime"]
Server["McpServer<br/>src/mcp/server.ts"]
Tools["Public Tool Registration<br/>src/tools/registerInventoryTools.ts"]
Resources["Context Resources<br/>resources/ via BSN_RESOURCES_DIR"]
Prompts["Local Prompts<br/>prompts/"]
ApiConfig["api-config Resource<br/>runtime metadata"]
Server --> Tools
Server --> Resources
Server --> Prompts
Server --> ApiConfig
end
Transport --> Server
Config["Environment Config<br/>src/config/env.ts"] --> Auth
Config --> Resources
Config --> ApiConfig
Tools --> ReturnShape["MCP Tool Return<br/>concise text + structuredContent"]
```
## Tool Workflow
```mermaid
flowchart TB
Need["User needs SKU/material or inventory"] --> Known{"Exact material ID known?"}
Known -->|"yes"| InventoryTool["getInventoryAvailability"]
Known -->|"no"| IdentifyTool["identifySkuMaterial"]
IdentifyTool --> LocalSku["Local resources/skus JSONL lookup<br/>brand file first, SanMar fallback"]
LocalSku --> LocalCandidate{"Confident local candidate?"}
LocalCandidate -->|"yes"| Candidate
LocalCandidate -->|"no"| SearchScope["Resolve fallback vector scope<br/>explicit vectorStoreIds, brand route,<br/>OPENAI_SKU_VECTOR_STORE_IDS, or discovery"]
SanMar["SanMar sanmar_skus vector store<br/>distributor/multi-brand fallback where available"] -.-> SearchScope
SearchScope --> Responses["OpenAI Responses API<br/>file_search"]
Responses --> Candidate{"Usable SKU/material candidate?"}
Candidate -->|"yes"| InventoryTool
Candidate -->|"no"| LastResort["searchBsnShopLastResort<br/>returns host-agent instructions only"]
LastResort -.-> HostSearch["Host agent searches BSN Shop<br/>outside this MCP server"]
HostSearch --> ExactFound{"Exact material ID found?"}
ExactFound -->|"yes"| InventoryTool
ExactFound -->|"no"| AskUser["Ask user for SKU, material ID,<br/>product link, or clearer details"]
InventoryTool --> BsnApi["BSN Inventory API<br/>/v1/private/inventory-inquiry/{material_id}"]
BsnApi --> Normalize["Inventory Normalization<br/>src/inventory/normalize.ts"]
Normalize --> InventoryReturn["Inventory result<br/>structuredContent source of truth"]
VectorFiles["getVectorStoreFiles"] --> StoreScope["Explicit vectorStoreIds,<br/>OPENAI_SKU_VECTOR_STORE_IDS,<br/>or OpenAI discovery"]
StoreScope --> StoresApi["OpenAI Vector Stores API<br/>metadata only"]
StoresApi --> StoreReturn["Vector store/file metadata<br/>no file contents"]
```
Dashed edges describe guidance or host-agent activity outside this MCP server process.
## Tools
- `identifySkuMaterial`
- `getVectorStoreFiles`
- `searchBsnShopLastResort`
- `getInventoryAvailability`
Use `identifySkuMaterial` when a user provides a natural-language product description, partial SKU, or uncertain style/color/size wording. It searches local brand JSONL catalogs in `resources/skus` first and returns immediately when the local candidate is confident. If local candidates are weak, ambiguous, or unavailable, it searches explicit `vectorStoreIds`, brand-routed vector stores, configured OpenAI vector stores, or discovered OpenAI vector stores and returns closest SKU/material candidates with evidence. It does not check live inventory.
Use `getVectorStoreFiles` to inspect accessible OpenAI vector stores and their attached vector store file metadata. It does not fetch file contents. It uses explicit `vectorStoreIds` input first, then `OPENAI_SKU_VECTOR_STORE_IDS`, then OpenAI vector store discovery.
Use `searchBsnShopLastResort` only when local SKU lookup and explicit, configured, or discovered vector-store search, including the SanMar distributor/multi-brand fallback where relevant, return no usable SKU/material candidates. It does not scrape or browse from the MCP server. It returns instructions for the host agent to search `https://www.bsnsports.com/shop/`, preserve exact product identifiers from matching products, resolve styles/SKUs to a material ID when needed, and then call `getInventoryAvailability` only after an exact material ID is found.
Use `getInventoryAvailability` only after the exact material ID is known. Pass optional `color` and `size` when the user asks for a specific variant quantity. It calls:
```text
/v1/private/inventory-inquiry/{material_id}
```
The text response contains a compact inventory summary and, when `color`, `size`, or a full material ID can identify a variant, the matched inventory cell. Matched cells include the split stock levels and combined total when available, for example `Matched availability: available 1778, IP 1528, DS 250, total 1778.` Agents should report IP and DS separately in addition to the combined total for inventory questions. If the upstream pretty payload only exposes segment-coded `IP` and `DS` rows, the normalizer maps them to `ip` and `ds`; when explicit `available` and `total` fields are absent and both values are numeric, it computes `available` and `total` as `IP + DS`. If the requested text does not match the inventory matrix labels, the matcher also tries color and size codes embedded in full material IDs such as `NKCJ1611657LRG`. `structuredContent.data` is the source of truth for normalized product, size, color, inventory-cell fields, and `matchedAvailability`.
## Resources
Inventory-only context resources are exposed under:
```text
context://bsn-inventory-mcp/resources/<relative-path>
```
Use `resources/instructions.md` as the entry resource and `resources/tool-map.md` for the tool workflow. `BSN_RESOURCES_DIR` controls local context resources and the server-side `resources/skus/*.jsonl` SKU catalogs. Those SKU catalogs are searched before OpenAI vector stores; large JSONL files are intended for server-side lookup, not whole-file model context.
Secret-free runtime metadata is exposed under:
```text
config://bsn-inventory-mcp
```
The config resource includes registered operations, prompt metadata, environment variable names, selected inventory environment, logging settings, local SKU catalog metadata, brand vector-store routing metadata, and vector-store discovery settings. It does not include credential values.
## Prompts
Only inventory/SKU prompt templates belong in `prompts/`. `check-inventory` is the default prompt for inventory lookup.
## Checks
```bash
npm run check
```
The combined check runs:
```bash
npm run typecheck
npm run typecheck:test
npm test
npm run build
```
GitHub Actions runs `npm ci` and `npm run check` on pushes to `main` and pull requests.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessSyncing