Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,9 @@ jobs:
linux-amd64/helm lint charts/storelink -f charts/storelink/values-local.yaml
linux-amd64/helm lint charts/storelink -f charts/storelink/values-prod.yaml
linux-amd64/helm template storelink charts/storelink -f charts/storelink/values-prod.yaml > /dev/null

linux-amd64/helm template storelink charts/storelink --set stores.enabled=false > /dev/null
if linux-amd64/helm template storelink charts/storelink --set stores.scheme=https --set stores.tlsSecretName= --set stores.tlsClusterIssuer= > /dev/null 2>&1; then
echo 'HTTPS store configuration without certificate settings must fail' >&2
exit 1
fi
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ StoreLink runs a WooCommerce store on Kubernetes and puts a team MCP gateway in

## Features

- **Multi-store provisioning:** create, retry and delete independent stores from **Stores**, with per-user/team limits, automatic MCP registration and namespace isolation. [Guide and verification](docs/multi-store.md).
- **WooCommerce store:** WordPress, WooCommerce and MariaDB in the cluster, with persistent storage and cash-on-delivery checkout.
- **WooCommerce MCP server:** list products, create product, list orders, get order and update order status, served over Streamable HTTP. Reachable only from the gateway.
- **MCP gateway:** aggregates the tools of every enabled upstream behind one endpoint, namespaces tool names per server, and routes each call to its server. Every upstream call has a timeout. Runs as two replicas that share MCP sessions, rate limits and configuration changes through PostgreSQL.
Expand Down Expand Up @@ -32,7 +33,7 @@ flowchart LR

| Component | Responsibility |
| -------------------------- | ------------------------------------------------------------------------------------ |
| `apps/dashboard` | React dashboard: connections, access, activity and chat |
| `apps/dashboard` | React dashboard: stores, connections, access, activity and chat |
| `services/gateway` | Express backend, aggregated MCP endpoint, upstream registry, audit log and chat loop |
| `services/woocommerce-mcp` | MCP server for store products and orders, using the WooCommerce REST API |
| `services/time-mcp` | Store timezone and day-window tools |
Expand Down Expand Up @@ -147,6 +148,10 @@ The key is stored in a Kubernetes Secret and used only by the gateway. Example p

Each answer lists the tools it called, and **Activity** shows the matching audit records. Conversations and token usage are saved per user.

### Provision additional stores

Open **Stores → Create store**, choose a name and short address, and wait for Ready. Members manage their own stores; admins manage their team’s stores. Each store gets independent persistent data and a tool prefix such as `woo-north__*`. Retry failed setup or confirm deletion from its card. Defaults allow two stores per user and three per team; Failed/Deleting stores count until removal finishes. See [multi-store setup, production TLS, backups and acceptance](docs/multi-store.md).

### Administration

- **Connections:** add, edit, disable or remove MCP servers, each with an optional static header or bearer credential. The gateway pings each server every second, so a stopped or restarting server shows as Unreachable within about a second, and every status change is kept in the connection history. Public HTTPS servers can be added directly. Cluster-internal origins must be listed in `gateway.extraUpstreamOrigins`. Credentials are encrypted with AES-256-GCM, stored in a Kubernetes Secret, and never returned by the API.
Expand Down Expand Up @@ -351,6 +356,7 @@ node scripts/smoke-cluster.ts # store tools and storefront checkout
node scripts/smoke-dashboard.ts # dashboard, invitations, keys and server management
node scripts/smoke-failures.ts # upstream outage, recovery and persistence
node scripts/smoke-pod-delete.ts # deleted upstream pod shows as Unreachable, then recovers
node scripts/smoke-multistore.ts # two-store lifecycle, isolation, retry/restart and one paid comparison
node scripts/smoke-scaling.ts # sessions, configuration changes and limits across two gateway pods (one live model call)
node --env-file=.env scripts/smoke-chat.ts # live chat flow (uses the OpenAI API)
```
Expand All @@ -373,12 +379,14 @@ kubectl -n storelink exec deploy/storelink-postgres -- \
| #7 Scaling | Implemented |
| #8 Observability | Implemented |
| #9 Upgrades and rollback | Documented and rehearsed |
| #2–#5, multi-store bonus | Not attempted |
| Multi-store bonus | Implemented and verified locally |
| #2–#5 | Not attempted |

[Verification](docs/verification.md#stand-out-items-and-additional-features) lists what each item covers and where it is implemented, along with additional features such as catalog change notifications, per-user conversations with token accounting, and single-use invitations.

## Documentation

- [Multi-store provisioning](docs/multi-store.md)
- [System design and tradeoffs](docs/system-design.md)
- [Threat model](docs/threat-model.md)
- [Verification](docs/verification.md)
Expand Down
23 changes: 19 additions & 4 deletions apps/dashboard/src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useEffect, useState } from 'react';
import { createRoot } from 'react-dom/client';
import './style.css';
import { Stores } from './stores.tsx';

type User = {
userId: string;
Expand All @@ -14,6 +15,8 @@ type Me = {
chatConfigured: boolean;
model: string;
gatewayUrl: string;
storesConfigured?: boolean;
storeLimits?: { perUser: number; perTeam: number };
};
type Server = {
id: string;
Expand Down Expand Up @@ -259,7 +262,13 @@ function App() {
</div>
</div>
<nav aria-label="Main navigation">
{['Chat', 'Connections', 'Access', 'Activity'].map((name) => (
{[
'Chat',
...(me.storesConfigured ? ['Stores'] : []),
'Connections',
'Access',
'Activity',
].map((name) => (
<button
key={name}
onClick={() => {
Expand All @@ -270,9 +279,13 @@ function App() {
>
<span aria-hidden="true" className="nav-icon">
{
{ Chat: '✧', Connections: '⊞', Access: '♙', Activity: '≋' }[
name
]
{
Chat: '✧',
Stores: '▤',
Connections: '⊞',
Access: '♙',
Activity: '≋',
}[name]
}
</span>
{name}
Expand Down Expand Up @@ -325,6 +338,8 @@ function App() {
)}
{tab === 'Chat' ? (
<Chat me={me} run={run} />
) : tab === 'Stores' ? (
<Stores me={me} request={api} run={run} />
) : tab === 'Connections' ? (
<Connections me={me} run={run} />
) : tab === 'Access' ? (
Expand Down
Loading
Loading