A drop-in commerce core for MCP servers, built on the Agentic Commerce Protocol (ACP). Point it at a simple products file and your tool can take money inside the conversation that was already happening: buy an asset, upgrade to Pro, renew a license, buy credits. The commerce logic is a clean library; the MCP server and the ACP REST endpoints are thin faces over it.
Full documentation lives at afcommerce.com.
It is built for companies that already run (or are building) an MCP tool and will eventually want to charge for something inside it. The commerce rides along with a real brand and a real product, so the trust problem of an unknown storefront does not apply.
- Not a standalone shopping destination for unknown sellers.
- Not a CRM, order database, or customer history store. On a successful payment we hand the order to the merchant and forget it.
- Not a frontend. There is no cart UI or chat widget, on purpose.
- Standalone server. Run the whole stack (
npm startor the Docker image, or the Lambda handler). Our code is the MCP server. - Embedded. Import the commerce core into your own MCP server and register the tools there. You never touch our transport.
- Library only. Call the core from your REST API or bot, no MCP at all.
cp config.example.json config.json # edit it for your store
cp .env.example .env # add your Stripe key (or leave blank for the mock)
docker build -t mcp-commerce .
docker run --rm -p 3000:3000 \
-v "$PWD/config.json:/app/config.json" \
--env-file .env \
mcp-commerceThe server then exposes:
| Path | What it is |
|---|---|
POST /mcp |
MCP endpoint (streamable HTTP, stateless JSON) |
/checkout_sessions... |
ACP REST checkout endpoints |
POST /webhooks/stripe |
Stripe webhook receiver |
GET /feed?format=jsonl|csv |
ACP product feed |
Without STRIPE_SECRET_KEY set, the server uses a mock payment provider (no real
charges), which is handy for local runs and demos.
import {
createCommerce,
createCheckoutService,
BundledCatalogSource,
SqliteCartStore,
SqliteCheckoutSessionStore,
MockPaymentProvider,
NoTaxCalculator,
NoopOrderSink,
registerCommerceTools,
} from "mcp-commerce";
const catalogSource = new BundledCatalogSource({ path: "./products.json" });
const commerce = createCommerce({
catalogSource,
cartStore: new SqliteCartStore({ path: "./carts.sqlite" }),
config: { currency: "usd", cartTtlSeconds: 3600 },
});
// Register the shopper-facing tools on your own MCP server:
registerCommerceTools(yourMcpServer, commerce /*, checkoutService */);The AWS-backed drivers (DynamoDB stores, S3 catalog source) live in a separate entry so the optional AWS SDK only loads when you use them:
import { DynamoCartStore, S3CatalogSource } from "mcp-commerce/aws";Front an API Gateway HTTP API (payload format v2) at the exported handler:
handler = dist/lambda.handler
Set COMMERCE_CONFIG (and the secret env vars) the same way the server uses them.
On Lambda, use the DynamoDB stores for carts, sessions, and idempotency (set
storage.type to dynamo); an in-memory SQLite store would not survive between
invocations. The S3 catalog source fits the same stack. The handler routes the
ACP endpoints, the Stripe webhook, the feed, and the MCP endpoint, all through the
same core as the standalone server.
Catalog only, no orders or config mixed in. Prices are integer minor units (cents) to match Stripe and avoid floating point money bugs.
{
"currency": "usd",
"products": [
{
"id": "pro-license",
"title": "Pro License",
"description": "Full license with all features unlocked.",
"url": "https://example.com/products/pro-license",
"image_url": "https://example.com/img/pro-license.png",
"price": 4999,
"available": true,
"variants": [
{ "id": "pro-single", "title": "Single seat", "price": 4999 },
{ "id": "pro-team", "title": "Team (5 seats)", "price": 19999 }
]
}
]
}A product with variants is bought by variant id; a product without variants is
bought by its own id. See examples/products.example.json for a full sample.
Non-secret settings live in config.json (see config.example.json). Secrets only
ever come from environment variables (see .env.example), never a committed file.
Config highlights:
currency,cartTtlSecondscatalog:{ "type": "bundled" | "remoteUrl" | "s3", ... }storage:{ "type": "sqlite" | "dynamo", ... }tax:{ "enabled": false }(see Tax below)successUrl,cancelUrl,orderWebhookUrl,permalinkBaseacp:{ "requireSignature": false }links: terms, privacy, return policy links surfaced in the sessionfeed: merchant-level fields for the ACP product feed
Environment variables:
| Variable | Purpose |
|---|---|
PORT |
Server port (default 3000) |
COMMERCE_CONFIG |
Path to the config file (default ./config.json) |
CART_DB, SESSION_DB, IDEMPOTENCY_DB |
SQLite file paths (default in-memory) |
STRIPE_SECRET_KEY |
Stripe key; blank uses the mock provider |
STRIPE_WEBHOOK_SECRET |
Verifies incoming Stripe webhooks |
ACP_BEARER_TOKEN |
Bearer token agents present to the ACP endpoints |
ACP_SIGNING_SECRET |
Optional HMAC request-signature verification |
ORDER_WEBHOOK_SECRET |
Optional signature on the order we POST to the merchant |
The ACP endpoints are closed by default: with no ACP_BEARER_TOKEN set, every
request is rejected.
Two data types, stored separately and swappable per deploy:
- Catalog (read only): bundled file, remote URL, or S3 object.
- Cart and checkout session (short lived, TTL'd, not customer data): SQLite for Docker/EC2, DynamoDB for Lambda (native per-item TTL).
- Direct Stripe (everywhere): a hosted Stripe Checkout URL the shopper opens.
Used by the MCP
checkouttool and any merchant-facing surface. - Delegated (SPT) (US, Stripe preview terms): a third-party agent grants a
scoped Shared Payment Token, charged immediately by the ACP
completeendpoint.
Tax is delegated to Stripe Tax rather than reimplemented. It is off by default; set
tax.enabled and provide a Stripe key to turn it on. Tax is computed once the
buyer's address is known (from fulfillment_details on update, or the billing
address on complete) and folded into the line tax and the session total.
search_products, get_product, add_to_cart, view_cart, update_cart_item,
remove_from_cart, and checkout (registered when a checkout service is provided).
Because the transport is stateless, add_to_cart returns a cart_id that must be
passed back on later calls; the tool descriptions tell the agent so.
npm install
npm run dev # tsx src/server.ts
npm test # node --test
npm run typecheck
npm run build # tsc -> dist/
npm run feed # print the product feed to stdout- Documentation, the docs page for this module
- Auto Learning Agents, an AI agents OS and UI
- Adaptive Recall, a learning MCP memory tool
- Levity, a free AI image creation UI
- AI Apps API, the company and developer site behind these projects
Elastic License 2.0. Free to use, modify, and embed in your own product,
including proprietary and commercial ones. The one limitation is that you may not
offer the software to third parties as a hosted or managed commerce service. See
the LICENSE file for the full text.
Copyright (c) 2026 Paul Crinigan, aiappsapi.com