# Changelog — Bloques MCP server

Every change to the contract of the Bloques MCP server (`POST /api/mcp`): tool names, inputs,
outputs, error texts, server instructions and accepted credentials. The format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versions follow semantic
versioning over that contract — not over the Bloques app:

- **Major**: a tool or field removed or renamed, an input tightened (something accepted before
  now fails with `invalid_input`), the meaning of a field, an error code or an error text a client
  relies on changed, or a credential no longer accepted.
- **Minor**: a new tool, or a new optional input or output field.
- **Patch**: fixes that do not change the shape of the contract.

The current version is the `serverInfo.version` your client receives in the `initialize`
handshake (on protocol 2026-07-28, in `_meta["io.modelcontextprotocol/serverInfo"]` of every
result). The REST API (`/api/v1`) has its own changelog in §14 of
https://enbloques.com/llms-full.txt.

## [2.0.0] - Unreleased

### Changed (breaking)

- `bloques_create_product` and `bloques_update_product`: a product belongs to none, one or
  several categories. `category_ids` (the complete list, up to 20; on update, omit = keep and
  `[]` = none) replaces `category_id`, and the product output carries `categories`
  (`[{ id, name }]`) and `category_ids` instead of `category` and `category_id`
  (also in `bloques_list_products`).
- `bloques_create_document`: `notes` is now the remark sent to SUNAT (a `cbc:Note` in the XML)
  as well as printed on the PDF. Line breaks become spaces, and more than 200 characters after
  that fails with `invalid_input` (it accepted 1000, PDF only).
- Every tool name gets the `bloques_` prefix (e.g. `create_document` →
  `bloques_create_document`), so an agent with several servers connected can tell them apart.
  The old names are not aliases: calling one returns `unknown_tool`, and the text names the new
  tool. Fetch `tools/list` again to get the current names.
- Inputs are validated against the same schema `tools/list` announces, with Spanish reasons.
  Arguments that used to be ignored or silently clamped now fail with
  `invalid_input: <field>: <reason>`: invalid filters on
  `bloques_list_documents`/`bloques_list_quotes`, `per_page` above 100, malformed ids (they
  returned an internal error), and `from`/`to` that are not a real `YYYY-MM-DD` date (a timestamp
  used to be ignored and returned the list unfiltered). `bloques_create_product` rejects `images`
  and `bloques_create_expense` rejects `attachments`: files are uploaded in the app, not over
  MCP.
- Product `kind` gains a third value, `combo` (a product that bundles others at its own price;
  always `NIU`, no stock of its own, no modifiers or extras), in the output of every product tool
  and in the input of `bloques_create_product` and `bloques_update_product`. A client that
  switches on `kind` must handle it.
- `bloques_create_product` and `bloques_update_product` fail, with a text naming the product and
  what to do, when a write would break a combo — also one that does not send `components`: a
  different IGV `affectation` between a combo and one of its products, ISC/ICBPER on a product
  that is inside a combo, nesting a combo, turning a combo into a product or service without
  `components: []`, or giving a combo `modifiers` or `extras`.
- Tool error texts are rewritten in Spanish and say what to do next (which tool gives a valid
  id, who grants a missing permission, where a module is turned on). The `Error: ` prefix is
  gone: a text with a machine code is `<code>: <text>`. Codes do not change; key on them, not on
  the wording. An internal error in a write tool says the action may have happened and how to
  check it before repeating it.
- Over the rate limit, a `tools/call` returns a tool result with `isError: true` and
  `rate_limited: …` instead of HTTP 429. The limit is 120 tool calls per minute per connection
  (user × OAuth client, or the token); only `tools/call` counts. A legacy `sk_live_…` token is
  still also subject to the REST limit (120 requests per minute, every request counts, HTTP
  429).
- A request with an `Origin` header outside the allowed hosts (Bloques itself, `claude.ai`,
  `claude.com`, `chatgpt.com`, over https) is refused with 403. Clients that send no `Origin`
  (server-side clients, SDKs) are not affected.
- A request whose `MCP-Protocol-Version` header names a version this server does not serve is
  refused with HTTP 400 (JSON-RPC error `-32000`), as the 2025-06-18 and 2025-11-25 transports
  require; it used to be served. Without the header a request is treated as 2025-03-26.
  `initialize` keeps negotiating: an unknown requested version gets the newest one we serve,
  now 2025-11-25 (it was 2025-06-18).
- An OAuth connection expires 90 days after its most recent consent, even if the refresh token
  keeps rotating: the call gets 401 and the refresh `invalid_grant` (HTTP 400), and the user
  approves again.
- The OAuth authorization server moves to new endpoints, and **every OAuth connection has to be
  reconnected once**: tokens issued before this version no longer work (the call gets 401 and the
  client starts the flow again on its own). The issuer is now `https://<host>/api/auth`; its
  metadata (RFC 8414) is at `/.well-known/oauth-authorization-server/api/auth` (also at the root
  path and at `/api/auth/.well-known/oauth-authorization-server`), and the protected resource
  metadata names it in `authorization_servers`. Endpoints: `/api/auth/oauth2/register`,
  `/oauth2/authorize`, `/oauth2/token`, `/oauth2/revoke`, `/oauth2/introspect` (the
  `/api/auth/mcp/*` ones are gone). The authorization response carries `iss` (RFC 9207).
- Scopes are the Bloques vocabulary plus `offline_access`. `openid`, `profile` and `email` are no
  longer accepted (`invalid_scope`): there are no ID tokens or userinfo.
- `prompt=none` on the authorize endpoint is refused with 400 `invalid_request`, and so is an
  authorize sent by POST (405): every authorization shows the consent screen.
- A refresh token error (`invalid_grant`) is HTTP 400, as RFC 6749 says (it was 401).
- A `resource` other than `https://<host>/api/mcp` is refused with `invalid_target`.
- Text written by staff or customers (names, descriptions, notes, addresses…) arrives
  sanitized: control and invisible formatting characters are replaced, and each human-text field
  is a single line or bounded paragraphs, capped at its own input limit. REST v1 is unchanged.
- `bloques_get_document_files` and `bloques_get_quote_files` return signed download URLs
  (`/api/mcp/files/…`) instead of `/api/v1/…` URLs. Open them with no `Authorization` header;
  they expire 15 minutes after the call, at the new `expires_at` field. After that the URL
  answers 410 `link_expired`: call the tool again. A tampered or foreign URL is 403
  `invalid_link`. `bloques_get_quote_files` returns `pdf_minimal` as its own signed URL; appending
  `?template=` no longer changes the layout.
- Documents and quotes returned by the tools (`bloques_create_document`, `bloques_get_document`,
  `bloques_list_documents`, `bloques_create_quote`, `bloques_get_quote`, `bloques_list_quotes`,
  `bloques_update_quote`) no longer carry `files`: those were `/api/v1/…` paths the OAuth token
  cannot open. Use `bloques_get_document_files` or `bloques_get_quote_files`.
  `bloques_create_document`'s `files_note` now points to `bloques_get_document_files`.

### Added

- `bloques_create_document`: optional `collection_payments` (`[{ method, amount }]`, two or more
  distinct payment methods summing exactly to the total) records a split payment. With it,
  `collection_method` is derived (the method with the largest amount); omitting it behaves as
  before.
- `bloques_create_expense`: each line of `items` accepts an optional `modifier_id`, one of the
  modifiers (presentations) of its `product_id`. With the inventory module active, the purchase
  enters that presentation's stock. A modifier of another product, or without `product_id`, fails
  the call; omitting it behaves as before.
- Combos over MCP: `bloques_create_product` and `bloques_update_product` accept `components`
  (`[{ product_id, quantity }]`, up to 30; the complete list — on update, omit = keep and `[]` =
  no longer a combo) and `kind: "combo"`, with the same rules as `POST /api/v1/products`. Every
  product tool (`bloques_list_products` included) returns `components`
  (`[{ product_id, quantity, position }]`, `[]` for a regular product).
- Client ID Metadata Documents: a client can use the https URL of its metadata document as its
  `client_id` instead of registering (the authorization server metadata announces
  `client_id_metadata_document_supported`). The document must have `client_id` equal to its URL,
  `client_name` and `redirect_uris`, be JSON of at most 5 KB, and be served from a public domain
  without redirects. Dynamic client registration stays open.
- The `resource` parameter (RFC 8707) is accepted on authorize and token, and recorded on the
  token.
- Protocol 2025-11-25, through `initialize`, next to 2025-06-18, 2025-03-26 and 2024-11-05.
- Protocol 2026-07-28, statelessly per request: no `initialize`; each request carries its
  version and client capabilities in `_meta` and the `MCP-Protocol-Version`, `Mcp-Method` and
  (on `tools/call`) `Mcp-Name` headers. `server/discover` returns the supported versions,
  capabilities and instructions. It serves `server/discover`, `tools/list` and `tools/call`;
  results carry `resultType: "complete"` and the server identity in `_meta`, and
  `server/discover` and `tools/list` carry `ttlMs: 0` and `cacheScope: "private"`. Unsupported
  version: 400 `-32022` with `data.supported`; missing or mismatched header: 400 `-32020`;
  malformed `_meta`: 400 `-32602`; a method not implemented in this revision (`ping`,
  `initialize`, `subscriptions/listen`, `resources/*`, `prompts/*`, `completion/complete`): 404
  `-32601`. The empty `resources/list` and `prompts/list` answers stay on the 2025 revisions.
- `today` (Lima date, `YYYY-MM-DD`) in `bloques_list_documents`, `bloques_list_quotes`,
  `bloques_list_receivables`, `bloques_get_crm_pipeline` and `bloques_get_usage`; overdue receivables are computed with it. The instructions
  tell the agent to resolve "today", "yesterday" and "this month" from it.
- Every tool has a `title` and the four MCP annotations (`readOnlyHint`, `destructiveHint`,
  `idempotentHint`, `openWorldHint`), always explicit. `bloques_create_document` is destructive and
  open-world.
- `inputSchema` is generated from the validating schema, so it now lists fields that were
  accepted without being announced (e.g. `bloques_create_document`: `collection_method`,
  `create_receivable`, `from_quote_id`, `from_sales_note_id`, and `note` and
  `attributed_staff_id` per item) with their real limits.
- Every answer is checked against a declared output shape; undeclared fields never leave. An
  answer off its shape is `off_contract: …` (`isError: true`); for a write tool the text says
  the action did happen and must not be repeated.
- `bloques_search_customers` returns `total`, the number of matches.
- `scope="…"` in the `WWW-Authenticate` header of a 401.
- A failure to check the credential is HTTP 503 with `Retry-After` and JSON-RPC error `-32000`,
  never 401: the connection is still valid, retry after that delay.
- Tools that return third-party text end their description with a notice that it is data, not
  instructions.

### Changed

- The text content block of a tool result is compact JSON (it was indented); the same value is
  in `structuredContent`.
- Server instructions and `bloques_create_document` quote this company's effective IGV rate and the
  current per-bag ICBPER amount, instead of a fixed 18% and S/ 0.50.

### Deprecated

- `sk_live_…` tokens at `/api/mcp` are deprecated: connect with OAuth. They keep working in
  2.0.0 and will stop being accepted in a future major version, announced here before it
  happens. The REST API keeps accepting them.

### Fixed

- `bloques_search_customers` searches the whole directory (name, nickname or document number,
  case-insensitive), sorted by name. It used to scan at most 200 customers, so an existing
  customer could be missing from the results.
- `bloques_create_document` with `from_sales_note_id` fails with `validation` («Esta nota de venta
  ya tiene un comprobante en camino…») while another receipt from the same nota de venta is still
  `processing`. Both could be accepted before, leaving one sale with two receipts. A rejected or
  `error` receipt does not block a new conversion.

### Security

- The MCP OAuth access token opens `/api/mcp` and nothing else: the REST API (`/api/v1/*`) no
  longer accepts it and answers 401, as for any invalid token (the app's own routes never did). A signed download URL carries the
  decision made when the tool was called: revoking the connection does not invalidate URLs
  already issued, which keep working until they expire (at most 15 minutes).
- Removing a member from the team, or a password reset, revokes all of that user's MCP
  connections. Before, a live connection followed the person to their next company.
- Revoking a connection or a token takes effect on the next call (it could take up to ~75 s).
- Bloques can block an MCP client app for everyone: its tokens stop working at once and every
  user has to reconnect after it is unblocked.
- The consent screen is titled with the host the authorization returns to, not the name the
  app declared, and marks any client other than `claude.ai`, `claude.com` or `chatgpt.com` as
  an unverified app. A client whose metadata document is served from one of those hosts counts
  as verified, unless it returns to a program on the user's computer.
- OAuth access tokens, refresh tokens and authorization codes are stored hashed (SHA-256): a
  copy of the database holds no usable token.
- Presenting a refresh token that was already used revokes the whole connection (every refresh
  and access token of that user and client), as RFC 9700 recommends: the legitimate client
  reconnects, a thief does not get in.
- The consent screen is shown on every authorization, also for a client the user already
  approved.

## [1.0.0] - 2026-08-21

- Baseline: MCP over Streamable HTTP (stateless) with OAuth 2.1 (dynamic client registration,
  PKCE, consent screen, single-use refresh tokens) and legacy `sk_live_…` tokens; tools
  filtered by the connection's scopes and its owner's role. Additive changes after this date
  (e.g. branch access, `list_issuers` and `issuer_ruc`) shipped without a version bump; 1.x
  ended with 28 tools.
