Skip to main content
Glama
dragosh29

Breww MCP server

by dragosh29

Breww MCP server

An MCP server that lets Claude, ChatGPT and other MCP clients work with a Breww brewery account: products and stock, production batches, trade customers, orders (which in Breww are also the invoices) and deliveries, and (when enabled) creating a sales order. It is built from Breww's public API documentation: the OpenAPI 3.0.3 document at https://breww.com/api/schema/ ("Breww public API", version "v0.1.0 beta") and the guide at https://breww.com/docs/breww-public-api/.

Once it's connected, someone at the brewery can ask things like:

  • "Which pubs are overdue to reorder, and who looks after them?"

  • "What does the Red Lion owe us, and which invoices are past due?"

  • "What's in the fermenters right now, and what's planned for next week?"

  • "How much Maris Otter do we have, and where is it?"

  • "What's going out on Friday's deliveries?"

  • With writes enabled: "Draft an order for the Red Lion: three firkins of Session Pale and two cases of Hazy IPA."

Tools

Tool

What it does

API calls

list_products

Sellable products (cask, keg, smallpack, multi-pack, stock item, service) with code, type, price and the drinks they contain. Filters: name, code, type, tag (by name, assumed; see Status); obsolete products left out unless asked for.

GET /products/

get_product

One product: barcode, volumes, duty, the drinks and stock items it is made of, tags, custom fields.

GET /products/{id}/

stock_levels

Current stock of stock items (ingredients, packaging, chemicals, guest beer, merchandise), summed per item and location, optionally with the individual lots (with a location filter, the lot counts say which cover that location and which every location); and the stock of finished products as Breww reports it. Filters: stock item, location, batch code, expiry date, product name.

GET /stock-received/?is_empty=False, GET /products/?include_fields=quantity_in_stock_in_format

list_batches

Drink batches (brews) with drink, status, dates, volumes and current vessel. Filters: status, drink, batch reference, start and planned dates.

GET /drink-batches/

get_batch

One batch: brew type, ABV, starting and final gravity, volumes, vessel and fill, recipe version.

GET /drink-batches/{id}/

list_customers

Customers, leads and suppliers (one list in Breww) with last order, predicted next order, cadence, average order value and Breww's churn and overdue probabilities. Filters: name, entity type, last order date, next expected order, churn and overdue probability, sales person, tag (by name, assumed).

GET /customers-suppliers/

get_customer

One customer in full, with its most recent orders.

GET /customers-suppliers/{id}/, GET /orders/?customer={id}&ordering=-issue_date

list_orders

Orders/invoices with status, payment status, dates, value, total and amount due. Filters: customer, order status, payment status, source, issue and due dates, amount due, number, PO number.

GET /orders/

get_order

One order/invoice with its lines, adjustment lines (deposits, delivery), totals and delivery.

GET /orders/{id}/

list_deliveries

Deliveries, collections, courier shipments and uplifts ("fulfillments") with date, customer, order, dispatch and completion, drop window and lines. Filters: date range, type, completed, failed, run, courier, order.

GET /fulfillments/

create_order

Creates a sales order, as a draft by default, with Breww's automatic customer emails suppressed by default. Fetches the customer and the products first and refuses locally if the record is not a customer, order processing is blocked (or draft-only and a confirmed order was asked for), or a product is unknown, obsolete or "packaged only". Only registered when writes are enabled.

GET /customers-suppliers/{id}/, GET /products/?id__in=…, POST /orders/

Every tool refuses arguments it does not know (a misspelt filter name is an error, not an unfiltered list) and text filters that are empty after trimming.

list_products, list_batches, list_customers, list_orders and list_deliveries also take ordering (the API's documented sort parameter: a field name, - for descending, several separated by commas), max_results and page.

There is no separate list_invoices: Breww's GET /orders/ is documented as "API endpoint to manage your orders/invoices" and returns PaginatedInvoiceList, so list_orders with order_status invoiced is that list.

Not covered on purpose: the other 121 of the spec's 132 operations, including accountancy sync, customer payments and payments, credit notes, purchase orders and supplier invoices, users, CRM activities and deals, fermentation readings, vessels, containers and ingredient batches, and every PUT, PATCH and DELETE.

Related MCP server: DailyMeals MCP

Setup

Requires Node 18 or later.

npm install
npm run build

You need an API key. In Breww, an Admin goes to Settings > Breww Apps & API, creates a Private app, then creates an API key on the app's page. Keys start with BRW. and are sent as Authorization: Bearer <key>. Each key has one access level, chosen when it is created and not changeable afterwards: Read only (GET, HEAD and OPTIONS only) or Full access. A Read only key makes writes impossible whatever this server allows; use one unless you want create_order. Breww reviews every app, private ones included; the docs say apps "can be used prior to the review", and that a rejected app's requests are refused.

Claude Desktop: add this to claude_desktop_config.json:

{
  "mcpServers": {
    "breww": {
      "command": "node",
      "args": ["/absolute/path/to/breww-mcp/dist/index.js"],
      "env": { "BREWW_API_KEY": "BRW.your-key" }
    }
  }
}

Claude Code:

claude mcp add breww -e BREWW_API_KEY=BRW.your-key -- node /absolute/path/to/breww-mcp/dist/index.js

Variable

Required

Meaning

BREWW_API_KEY

yes

Your API key, sent as a Bearer token. A value pasted with its Bearer prefix is accepted.

BREWW_ALLOW_WRITES

no

true to register create_order. Off by default.

BREWW_REQUESTS_PER_MINUTE

no

How many requests this server allows itself per minute, 1 to 600. Defaults to 60, Breww's documented limit; lower it if other integrations use the same account.

BREWW_TOOL_BUDGET_S

no

Time budget per tool call in seconds, above 0 and at most 55. Defaults to 45 (see Safety defaults). Lowered by the tests.

BREWW_BASE_URL

no

Defaults to https://breww.com/api. Used by the tests. Must not contain a username or password; the server refuses to start if it does.

Safety defaults

  • Read-only unless BREWW_ALLOW_WRITES=true. Read tools carry the MCP readOnlyHint annotation; create_order is marked as a non-idempotent, non-destructive write. With a Read only key Breww refuses the POST (the docs: "A 403 (or similar) on POST, PUT, PATCH or DELETE from a read-only key is expected behaviour") and the error says a Full access key is needed.

  • create_order creates a draft unless asked for confirmed; it never invoices. It sends allow_automatic_emailing_to_customer: false unless email_customer is true (the API's own default is true, so Breww's configured customer emails would otherwise go out).

  • Personal data is only returned when a tool is called with include_contact_details=true: customers' primary email, phone number, billing and delivery addresses, billing name override, tax number and custom fields; the names, job titles, emails and phones of a customer's contacts (by default only their number is returned); an order's contact email, billing and delivery addresses and its invoice PDF and customer-facing links (the documents carry the addresses; they are never downloaded); a delivery's full address, phone number, coordinates and delivery-note and invoice PDF links (by default only the town); and staff names (sales person, batch creator, order creator, delivery completed by), which are returned as user IDs by default.

  • In free text (customer names, comments and alert notes, order notes and private notes, order line notes, batch references, delivery instructions, stock lot notes and the names and values of product custom fields) email addresses are replaced with [email redacted], phone-number-like sequences with [phone redacted] and UK postcodes with [postcode redacted] by default; the raw text comes with include_contact_details. Nothing else is taken out of free text: a person's name or a street address typed into a comment or delivery instruction ("Call Sam", "12 Mill Lane") is returned as written. The phone match is a heuristic (the same as the Signable server's): international numbers written with + or 00 (including +44 (0)7700 …), UK numbers with a bracketed area code such as (0117) 496 0000, and UK-style 0… numbers of 9 to 11 digits with spaces, dots or hyphens between groups. The postcode match is a heuristic too, on the shape of a UK postcode in either case (BS3 4QT, bs34qt); a short code of the same shape, such as FV1 2HL, is redacted as well. Product custom fields are always redacted (the product tools have no switch). Breww's error messages are redacted the same way before they are passed on, and the API key is replaced with [redacted] in any error text that echoes it.

  • Never returned, even on request: payment records (payments_refunds), payment integration data (payment_integration_details) and whether a customer pays by direct debit. No bank or card endpoint is called.

  • IDs must be positive whole numbers (every ID used here is an integer key in the spec) and are checked before any call. Date filters must be real calendar dates (2026-02-30 is refused). Filters typed date-time accept a date alone, which is sent as the start of that day in UTC (2026-09-01 becomes 2026-09-01T00:00:00Z). ordering must be field names separated by commas.

  • Every tool call has one time budget, 45 seconds by default (BREWW_TOOL_BUDGET_S), shared by all its requests and waits, because the MCP SDK's default request timeout is 60 seconds and one call can make several requests (create_order makes three, a list up to ten pages). A retry wait or a wait for the rate window that would end past the budget is not started, a request still unanswered at the budget is abandoned, and the call ends with an error that says so, before the MCP client gives up. POST /orders/ is only sent if at least 15 seconds of the budget are left (a third of the budget, if that is shorter), so an order is not sent that the server might have to abandon; if it is abandoned in flight anyway, the error says the order may exist and to check list_orders first. A list that runs out of time after its first page returns the pages it has, with a note and the next_page to continue from. A call cancelled by the MCP client ends its waits at once, aborts a request in flight and sends nothing more, so a POST /orders/ that has not been sent by then is not sent (one already sent may still be processed by Breww).

  • Rate limits, as Breww documents them: 60 requests per minute and 5,000 per day; above them the API answers 429 with the message Request was throttled. and possibly the wait "in both the message and in a Retry-After HTTP header". This server keeps itself under the minute limit with a sliding one-minute window (BREWW_REQUESTS_PER_MINUTE, default 60); if the window is full and the next slot is more than 15 seconds away, the call fails at once with the wait in the message rather than hanging. The daily limit is not tracked. A 429 is retried at most twice for any method, including POST /orders/, on the assumption that a throttled request was not processed (see Status), waiting for Retry-After (whole or fractional seconds, or an HTTP-date; 2 s then 4 s when the header is absent or unreadable). Each wait is capped at 10 seconds; if Breww asks for a longer wait the call gives up at once and says how long to wait.

  • 502, 503 and 504 are retried the same way for GET only; after three failures the error says the service may be unavailable, without the gateway's HTML. A POST /orders/ is never retried after a gateway error, because the order may already exist; the error says to check list_orders first.

  • A 200 whose body is not a JSON object (a proxy or a login page, or JSON null, a string or a list) is an error that names BREWW_BASE_URL and describes the body by type and size, or by JSON kind, without quoting it. A rejected key produces a message that says where keys are created.

Tests

npm test

The test suite:

  1. Validates every fixture record against the component schemas in Breww's published OpenAPI spec (Product, Location, StockReceived, DrinkBatch, Customer for 230 records, Invoice with SaleBasic lines, Fulfillment) with Ajv's draft-04 dialect (OpenAPI 3.0 uses draft-04's boolean exclusiveMinimum/exclusiveMaximum) and ajv-formats, plus a key-by-key walk that fails on any field the spec does not declare. The spec is downloaded from breww.com/api/schema/ to spec.yaml on the first run. The spec needs translating before Ajv can read it; the translations, in test/schema.mjs, are: (1) nullable: true becomes "or null"; (2) type: "decimal" (not a JSON Schema type; 27 properties such as StockReceived.current_quantity) becomes "a decimal written as a string, or a number"; (3) type: "array", format: "binary" with an object example (34 properties such as Product.liquid_volume_gross, example {"litre": 100, "us_gallon": 26.4…}) is validated against the shape of its example, since the spec's type contradicts its own example; (4) format: "decimal" on a string becomes a numeric pattern; (5) for request bodies, readOnly properties are dropped from properties and required, as OpenAPI 3.0 prescribes; (6) the time format accepts a time without a zone (09:00:00). Negative controls check that the translated schemas still reject a string ID, a wrong example shape, a null in a non-nullable field and a non-numeric decimal, and that the key walk reports undeclared keys at any depth.

  2. Starts a local mock of the API under /api that serves those fixtures with the documented paging (page, page_size defaulting to 50 and capped at 200, count/next/previous/results, next null on the last page), Bearer-key auth (401 for a missing or wrong key; a Read only key may GET but gets 403 on POST; the spec's second scheme, tokenAuth with a Token prefix, is not modelled, because whether a BRW. key works under it is undocumented and the server always sends Bearer), 404 for unknown IDs, the run and courier delivery filters matching nothing (the Fulfillment schema has neither field, so no fixture delivery can carry one), the conditionally included quantity_in_stock_in_format only when include_fields asks for it, and a one-off 429 with Retry-After: 1 on GET /drink-batches/. The mock's list, detail and created-order (201) responses are validated against the response schemas the spec names for each operation. Breww publishes no error schema, so the 429 body is checked against a schema written from the documented message (Request was throttled.; the detail key holding it is an assumption) and the 400 body against a schema written from the release note of 2026-08-15, whose example is read from the spec and must pass verbatim; the mock's 400 for an unknown product is exactly that example. The 401, 403 and 404 bodies ({"detail": …}) are placeholders with no documented shape and are not validated.

  3. Starts the built server and drives it over stdio with the official MCP client: 35 checks, plus 7 on the built modules directly (listed after this step). They cover tools/list and annotations; every read tool; paging across pages 1 and 2 of 200 to the documented end (next null), whole-page continuation with next_page, a page past the end reported as such, and page_size following max_results; every filter each tool offers passed through exactly as the spec names it (status__in, beer, batch_ref__contains, datetime_started__gte/__lt, planned_start_date__gte/__lte, name__contains, code, type, tags, obsolete, is_empty=False, stock_item, batch_code__contains, expiry_date__lte, include_fields, entity_type__bitand, last_order_date__lt/__gte, next_expected_order__lte, prob_churned__gte, prob_overdue__gte, sales_person, customer, order_status__in, payment_status__in, source__in, issue_date__gte/__lte, due_date__lt, amount_due__gt, number, po_number__contains, date_scheduled__gte/__lte, type__in, completed, failed, run, courier, invoice, ordering), with __in values comma-separated as the spec's explode: false says and date-only values completed to date-times; stock summed per item and location with the location filter applied locally; redaction of emails, phone numbers, addresses, contacts, tax number, custom fields (names and values) and staff names by default and their return on request, with the national, +44 (0), bracketed and 00-prefixed phone forms and postcodes redacted in free text, and the street of a delivery instruction shown to be returned as written; payment records never returned, even on request; the write gate with the variable unset and set to false; the POST /orders/ body validated against InvoiceCreate and each line against SaleInlineCreate (readOnly fields left out), drafts and suppressed customer emails by default; the local refusals (blocked, draft-only and non-customer records, obsolete, packaged-only and unknown products, a discount without its type) with no POST made; a 400 in the documented index-keyed shape flattened into the message, and an error text that echoes the key, an email and a phone number passed on with all three redacted; the 403 from a Read only key on POST and the different 403 message on a GET; a 400 body with twelve messages cut to ten; bad IDs, impossible dates, malformed ordering, text filters that are only spaces or empty, and unknown arguments rejected before any call; the 404 message, and a 404 on page 1 of a list reported as not found rather than as a page past the end; an empty page ending a list walk even when next is set; the 429 retry waiting for Retry-After in the seconds, fractional-seconds and HTTP-date forms, and 2 s then 4 s when the header is missing or unreadable, giving up after three attempts on a persistent 429 and at once on a Retry-After above the cap, a 429 on POST /orders/ retried once; a 502 retried for GET and never for POST /orders/; a 504 then two 503s on a GET reported with advice; a 200 whose body is HTML, JSON null, a string or a list reported without quoting it; a create_order cancelled by the MCP client during a retry wait, with no further request and no POST; with a server whose budget is lowered to 3 seconds, create_order with 429s armed on all three of its requests ending in an error inside the budget with no POST, the POST not started when less than the write reserve is left, a POST still unanswered at the deadline abandoned with the "may already have been created" warning, and a list stopped for time after page 1 returning that page with next_page; the 401 message without the key; a key pasted with Bearer sent once; and that every request used Authorization: Bearer <key>, a documented method and path, and only query parameters the spec documents for that operation (include_fields excepted, which the guide documents in prose for /products/).

The 7 direct checks: the client-side rate window on the built client with a 1-second window (three requests go at once, the fourth waits for the window), with a one-minute window (the second request past a limit of one is refused at once, without a request), and with six concurrent requests against a limit of two per second (no one-second span carries more than two); the retry delay (a Retry-After of exactly 10 seconds is honoured, 10.5 gives up, 2 s then 4 s without a readable header, an HTTP-date) and that the default time budget is at most 55 seconds; a cancellation ending a 5-second retry wait at once, with no further request, and aborting a request in flight; redactDeep cutting lists nested deeper than 20 levels; the delivery type labels compared with the ones the spec documents; and start-up refused (a separate process) without a key, with an out-of-range BREWW_REQUESTS_PER_MINUTE or BREWW_TOOL_BUDGET_S, or with credentials in BREWW_BASE_URL. The MCP clients in step 3 that make most of the calls run with BREWW_REQUESTS_PER_MINUTE=600, because the suite makes more than 60 requests through one server process; the clients for the write gate, the Read only key and the wrong key run with the default of 60.

Status

This is a working prototype. It has not yet been run against the live API, because it was built without a Breww account. Everything below is taken from the published spec and guide and should be confirmed on a real account (Breww offers a free trial; whether a trial account can create a Private app and key is unconfirmed):

  • Authentication end to end with a real BRW. key, and the bodies of 401, 403 and 404 responses, which are not documented (the mock uses {"detail": …}; the server passes on a detail string, or else the string values of a JSON error body flattened as for the documented 400 shape, at most ten, redacted; a non-JSON error body is never quoted). That a Read only key's POST is answered with 403 (the guide says "403 (or similar)").

  • The 429: the key that holds Request was throttled., the form of Retry-After, and whether a throttled POST /orders/ is never processed (the server assumes so and retries it at most twice). Whether the 60-per-minute limit is per key or per account: this server counts only its own requests, per process.

  • Paging: what the API does with page_size above 200 (the mock caps it at 200) and with a page past the end (the mock answers 404 "Invalid page.", a guess; the server stops at next null on its own, and a 404 on a later page asked for by the caller is reported as a page that does not exist). Which fields ordering accepts on each endpoint; the documented examples are number, issue_date and value on orders.

  • quantity_in_stock_in_format: the guide names it as a conditionally included field on /products/, but the spec does not define it, so its shape is unknown. The mock returns a plain number; the server passes whatever comes back through untouched.

  • Value formats the spec leaves unclear: whether type: "decimal" values (stock quantities and prices) come as strings or numbers (the fixtures use strings such as "125.500", which the server converts to numbers); whether the array/binary fields (volumes, weights, gravities, ABV, current vessel, delivery address) really are objects shaped like their examples (the server reads litre, kg, value_decimal, reading_unit, name, city and so on from them); whether time values carry a zone.

  • Fields the spec marks as required and not nullable that a live account may leave empty: DrinkBatch.third_party_brewery (so every fixture batch has a partner brewery), DrinkBatch.current_vessel_info, and Fulfillment.completed_by/completed_date on deliveries not yet completed (the server shows them only when completed is true). Also Customer.entity_type, whose enum lists only single flags (1, 2, 4, 8, 16, 32) although its description says flags combine (a customer and supplier is 3); the fixtures follow the enum, and the server decodes the value as a bit field.

  • Filter semantics: whether tags (on products and customers) takes a tag's name, as the server assumes and the mock implements, or a slug or an ID, and whether it matches exactly (the spec gives the parameter no description); what run and courier on deliveries match (the Fulfillment schema returns neither field, so the fixtures cannot show it); whether name__contains and the other __contains filters are case-sensitive; whether entity_type__bitand=1 matches every record with that flag set (assumed); whether booleans are read as true/false (sent for obsolete, completed, failed) and False (sent for is_empty, as that endpoint's description writes it); whether a date-time filter reads the Z of 2026-09-01T00:00:00Z as UTC.

  • GET /stock-received/ covers stock items (ingredients, packaging, chemicals, guest beer, merchandise). It has no location parameter, so the location filter is applied after fetching; on a large account max_results may need raising.

  • create_order: that POST /orders/ accepts the body as sent (customer, order_status 1 or 2, allow_automatic_emailing_to_customer, optional dates, PO number and notes, and lines with product, quantity and optionally product_original_unit_value, discount, discount_type, product_name); what number Breww gives a draft; which fields the 201 response carries (the mock follows InvoiceCreate); whether GET /products/?id__in=… returns obsolete products (the local check assumes it does, and otherwise reports an obsolete product as not found); and that block_order_processing 2 and 3 mean what their labels say.

  • The order detail path: the spec types /orders/{id}/'s parameter as a string matching ^[^/]+$ with no description; the server sends the integer Invoice.id.

  • Soft-deleted records are never asked for: the guide describes include_soft_deleted=true, but none of the operations used here lists it as a parameter.

  • The spec is a beta ("There may be breaking changes made to the schema/structure at any time"), with a breaking change to validation errors as recent as 2026-08-15.

Going to production

This version runs locally over stdio with the brewery's own API key. For breweries to connect from claude.ai or ChatGPT without handling keys, the next step is a remote server (Streamable HTTP) behind OAuth, hosted by Breww. Breww already runs an OAuth2 Authorization Code server with PKCE, read and write scopes, one-hour access tokens and rotating 30-day refresh tokens for public apps, so the remote server can use each brewery's own grant. Then a listing in the Claude and ChatGPT connector directories.

Licence

MIT. Built by Alexandru Dragoș (alexandru.dragos96@gmail.com) with an AI agent (Claude) working under his direction.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    B
    maintenance
    Provides a secured, read-only remote MCP endpoint for a single authorized GoHighLevel sub-account, exposing CRM tools such as contacts, opportunities, conversations, appointments, calendars, and notes via bearer-token authenticated requests.
    28 npm
    MIT
  • F
    license
    Not graded
    quality
    B
    maintenance
    Enables an MCP client to read live order data for a DailyMeals account and submit form data with confirmations, protected by bearer-token authentication and idempotent writes.
    -
  • F
    license
    Not graded
    quality
    B
    maintenance
    A remote MCP server that lets MCP clients self-register over OAuth 2.1 and call read-only tools for organization identity, place and region browsing, dashboards, metrics, and ranking, forwarding the caller's own Auth0 token so each service still sees the real user. It also exposes a bearer pass-through surface for first-party apps holding an existing token.
    -
  • A
    license
    Not graded
    quality
    A
    maintenance
    Enables MCP-capable clients to sign in with OAuth and perform purchaser tasks such as browsing catalogues, checking inventory, managing stocktakes and recipes, drafting requisitions, updating buy lists, exporting reports, and processing invoices. It also supports supplier access for listing and inspecting connections, catalogues, products, purchase orders, invoices, and entitled reports.
    MIT