docs: specify remote purchasing MCP service

This commit is contained in:
DEls 2026-10-05 00:51:44 +02:00
parent 6824d2c62e
commit 4970e61e70

View file

@ -0,0 +1,183 @@
# Remote purchasing MCP: design specification
Status: ready for user review; product implementation has not started.
## User outcome
Cloud agents, local Codex scheduled tasks, Hermes and cloud Cowork should be able to connect to MEGA without relying on the owner's Mac being awake. Agents should read purchasing data, retrieve source documents, upload documents, create and edit purchase orders, match delivery notes to orders and lines, post receipts and update received/outstanding quantities. Agents must not delete business records or documents.
The user approved starting the authenticated Coolify service and expanded purchasing tools described in the preceding discussion. This specification makes that approach reviewable. Stock is scoped to receipt and outstanding PO quantities in this release: no site/warehouse stock-on-hand, consumption, transfers or stock adjustments are inferred. Those need a separate inventory specification if requested.
## First page: decisions to review
- Deploy a separate, continuously running MCP service in Coolify using HTTPS and Streamable HTTP. Preserve the existing local stdio connection.
- Use a dedicated credential for each agent, with read-only or read/write permissions. PocketBase credentials remain on the server. The first release is for owner-controlled agents with access to all sites; it does not claim per-site authorization.
- Allow direct authenticated business writes through named tools. Do not add a mandatory human confirmation screen to scheduled workflows. Receipt posting requires an explicit tool argument and the current matching preview, rather than silently posting during document reading or matching.
- Preserve documents, posted receipts and audit history. Reject business deletion, including indirect deletion inside called routines. Temporary lock cleanup is allowed and is not exposed as a tool.
- Treat matching, processing and posting as distinct operations. A match or an AI processing result does not book a receipt by itself.
- Build and test locally first. A hostname, certificate or production-ready claim is only made after the Coolify deployment and external connection tests succeed.
## Present implementation and constraints
1. [src/mcp/server.ts](../../../src/mcp/server.ts) uses `StdioServerTransport` and registers only the 27 tools from [src/lib/agent/tools.ts](../../../src/lib/agent/tools.ts). It listens on no network port.
2. The app's Dockerfile packages a Next.js standalone server. It is not an MCP image and does not establish an HTTP MCP endpoint.
3. App routes authenticate through Next.js cookies and route-specific guards. Importing route handlers into an independent MCP process would couple it to Next.js request context and would not establish machine authorization. Shared operations must sit below both transports.
4. PO add/edit handlers validate line ownership and reject verified orders, then use `computeItemAmount` and `recomputePoTotals`. Those rules remain authoritative.
5. DN entity/item resolution checks batch membership, site, supplier and PO/line ownership and records decisions/aliases. Those validations must be preserved.
6. [src/lib/dn-pipeline/post-line.ts](../../../src/lib/dn-pipeline/post-line.ts) supports snapshot-bound, deterministic line receipt posting with lease fencing and source-proof PDFs. Repeated calls return the existing booking rather than another receipt.
7. [src/lib/dn-pipeline/promote.ts](../../../src/lib/dn-pipeline/promote.ts) deletes stale delivery lines before replacing them. It is not an allowed MCP posting path.
8. The DN upload route deletes an empty newly created batch on some failure paths. MCP uploads must preserve failed upload state and retry information rather than call that cleanup behavior.
9. `withBatchLease` uses deletion to release/reclaim `dn_pipeline_locks`. That infrastructure housekeeping is permitted; deleting DN batches, receipt lines, POs, documents or audit entries is not.
10. The Ask action gateway permits create/update/delete and undo may delete created records. Do not register it as a generic MCP write tool.
11. The local env file's ingest secret is a placeholder; the verified production secret exists in ignored `cooler.env`. Neither is reused as MCP authentication.
## Architecture
```text
Cloud agent / Hermes / Cowork / scheduled agent
|
HTTPS /mcp + MCP credential
|
Independent MEGA MCP service
authentication -> tool registry
|
Shared purchasing operations
validation / leases / audit / no-delete policy
|
PocketBase
Local stdio client -> same server factory and purchasing operations
App API handlers -> shared purchasing operations after their existing guards
```
Create a reusable server factory in `src/mcp/create-server.ts`. Keep transport lifecycle out of business handlers. The stdio entry point remains available; the HTTP entry point is `src/mcp/http-server.ts`. Use the installed MCP SDK's Streamable HTTP transport, not a hand-written JSON-RPC implementation. Stateless request transport is preferred: business concurrency and idempotency live in PocketBase, not in a process-wide MCP session map.
The service uses its own runtime image, `Dockerfile.mcp`, and `docker-compose.mcp.yml`. Package only the runtime source/assets needed by its imports, dependencies and the PDF rasterisation binaries used by the staged pipeline. Run as a non-root user on Node 22. Bind to `0.0.0.0` inside the container; Coolify exposes HTTPS. Container health is separate from authenticated MCP access.
Proposed public hostname: `mcp.mega.infra.apphub.co.za`; proposed path: `/mcp`. This address is not currently deployed. The hostname may be changed in configuration without changing tools.
## HTTP access and credentials
- `POST /mcp`: authenticated MCP initialize, discovery and calls.
- `GET /mcp`: follows the selected SDK transport's supported behavior; unsupported stream operations return the protocol-appropriate status.
- `DELETE /mcp`: do not support business deletion. If a later stateful transport uses this method, it may terminate a protocol session only; that is not a record deletion capability.
- `GET /health`: public minimal liveness response containing no credentials, records or detailed dependency errors.
- Authentication is checked before constructing tool handlers or contacting PocketBase. Missing/invalid credentials return 401.
- Use SHA-256 token hashes and constant-time comparison. `MCP_CLIENTS_JSON` supplies objects with `id`, `tokenHash` and `permissions`, where permissions are `read` or `read,write`. No actual credential appears in committed configuration. Invalid or placeholder configuration prevents HTTP startup.
- A read credential sees read tools only and cannot invoke writes by submitting an undiscovered tool name. A write credential includes reads and named business writes. Every tool invocation rechecks its required permission.
- Initial credentials grant all-site owner access. Do not offer a `siteIds` config option until all affected loaders and document paths enforce it.
- Configure `MCP_PUBLIC_URL`, `MCP_PORT`, `MCP_ALLOWED_ORIGINS`, PocketBase variables and optional AI variables. Honor deployment-provided env; `.env.local` is a development convenience only.
- Validate a supplied Origin against the configured allowlist, validate the expected host, bound request sizes and configure proxy timeouts for processing calls. Server-to-server clients may omit Origin.
- Issue separate credentials for Codex, Hermes and Cowork so one may be revoked independently. OAuth is outside the initial fixed-credential release; test each chosen client's request-header support before marking it connected.
## Tool contract
Preserve the existing read tool names so local clients keep working. Register MCP read-only/destructive annotations correctly. An annotation supplements enforcement; it is not authorization.
New purchasing tools use Zod schemas with unknown keys rejected. Record IDs are validated before database access. List tools have a bounded page size and deterministic pagination. Existing summary tools may continue computing all-site totals; paginated detail tools return record IDs needed by write and document calls.
### Read and document tools
| Tool | Required inputs | Result |
| --- | --- | --- |
| `list_purchasing_records` | Allowed entity, optional site/status, page and pageSize | Paginated IDs and operational fields; entities limited to POs, PO lines, delivery receipts/lines, DN batches/sources/pages/candidates, suppliers, supplier rules and document/item aliases |
| `get_purchasing_record` | Allowed entity and ID | Record with relevant relations and readable field metadata; no arbitrary collections or secrets |
| `get_purchase_order_detail` | PO ID | PO, all lines, received/outstanding quantities and document references |
| `get_delivery_candidate` | Batch ID and candidate ID | Source pages, extracted fields, checks, matching decisions, stored preview and posted state |
| `get_delivery_reconciliation` | PO ID | Existing reconciliation result comparing PO lines, posted receipts and unposted candidates |
| `get_document_download` | Typed document reference | Filename, MIME, size and authenticated same-origin download URL; never PocketBase admin/file tokens |
| `read_document` | Typed document reference, bounded page/byte range | MCP resource/blob content suitable for clients that cannot follow authenticated download links |
Typed document references cover a PO original, receipt slip/proof PDF, DN batch source, source page image and a candidate's assembled proof PDF. Resolve filenames from database records and encode path components. Do not accept arbitrary upstream URLs or caller-selected filesystem paths. Candidate proof PDFs use the selected source pages in saved order.
HTTP document downloads require the same MCP credential and stay on the MCP service origin. A URL alone does not grant access. Downloads are streamed with byte limits and correct Content-Type/Content-Disposition. Stdio clients receive bounded resource content because they have no HTTP service URL by default. Source documents stay immutable; an upload creates a new source rather than overwriting/deleting an old one.
### Create, edit, upload and matching tools
| Tool | Business behavior |
| --- | --- |
| `upload_purchase_order` | Validate PDF/JPEG/PNG/WebP, site and hash; preserve the original; create or return an existing queued PO; extraction status remains observable |
| `create_purchase_order` | Create a manually supplied order and validated lines, resolve site/supplier and compute totals using existing helpers |
| `update_purchase_order` | Edit allowed business header fields; supplier references are verified; source file, totals, audit and workflow internals cannot be arbitrarily set |
| `add_purchase_order_item` | Add a validated line to an unverified PO and recompute totals |
| `update_purchase_order_item` | Verify line membership, reject verified PO edits, update line and recompute totals |
| `set_purchase_order_verification` | Apply the existing verification rules; explicit true/false state, audited and serialized with PO edits |
| `upload_delivery_documents` | Create immutable DN sources in a site-scoped batch; hash deduplication; report failures without deleting batches or sources |
| `create_delivery_candidate` | Create a candidate linked to existing source pages and batch; validate page ownership; retain originals |
| `update_delivery_candidate` | Correct permitted document fields and unmatched extracted lines; invalidate stale previews; preserve posted line/source mappings |
| `set_delivery_grouping` | Save explicit page groups under existing assembly/revision guards; retain prior records and reject reuse of posted pages |
| `resolve_delivery_entities` | Select supplier/PO with existing site/supplier ownership checks; optionally remember the supplier alias |
| `resolve_delivery_item` | Select a PO line, preserve manual override provenance and optionally remember the item alias |
| `update_supplier_guidance` | Create/update purchasing-related supplier reconciliation rules and aliases using validated business fields |
| `process_delivery_candidate` | Produce a comparison/matching preview using existing saved context and processing logic; does not post a receipt |
| `process_delivery_batch` | Schedule the staged reading/grouping/checking pipeline for a batch; return durable operation status; never call legacy auto-promotion |
| `post_delivery_line` | Require the stored preview hash, selected source-line IDs and explicit posting confirmation; call `postLineWithinLease`; record actor and return receipt IDs |
| `correct_delivery_receipt` | Use the existing allowed correction rules, expected current quantities and audit event; reject immutable line receipts as the app does |
| `set_delivery_verification` | Apply existing delivery verification rules with audited true/false state |
| `get_mcp_operation` | Read status/result metadata for a caller's operation; no private credential information |
There are no delete, bulk-delete, source-removal, clear-all, raw-SQL, arbitrary HTTP request or generic Ask action tools. A request to cancel/dismiss processing is a state update preserving data, not physical deletion; expose it only through an explicit validated state tool if needed by the implemented grouping/processing workflow.
The tool scope covers purchasing business data and related supplier guidance. It does not expose writes to user accounts, authentication configuration, financial receipt internals, lock ownership fields or arbitrary workforce/fleet collections.
## Shared operations and no-delete enforcement
Extract reusable purchasing operations from relevant app routes into focused modules under `src/lib/purchasing/`. Accept an explicit actor, authenticated capability and PocketBase dependency; do not depend on `cookies()`, fake Request objects, browser PINs or forwarded app sessions. App routes retain their existing guards and call the shared operations. Tests pin existing API status/payload behavior during extraction.
MCP operations use an isolated database client with a capability boundary. Reject record DELETE requests and batch delete subrequests for every business collection. The only deletion allowlist is transient lock infrastructure owned by the operation layer (`dn_pipeline_locks` and the new MCP write-lock collection). Do not provide generic lock access to agents. Prevent file replacement/removal fields and deletion-by-cascade through validated mutation schemas. Test denial before a destructive request reaches PocketBase.
Shared helper use must not silently weaken the no-delete boundary. Select the newer per-line receipt posting path. Legacy replace-and-promote is not called by MCP. MCP upload failure cleanup retains failed batches/sources and marks their state. New internal schema fields and bookkeeping do not appear in generic update allowlists.
## Concurrency, retries and durable processing
- Every mutation accepts `operationId` (UUID) and a reason where the corresponding app operation requires one. Actor is derived from the authenticated credential ID; the client cannot impersonate another actor.
- Store operation identity, tool, normalized payload hash, status and result references in `mcp_operations`, with a unique key for credential/tool/operationId. Same ID and same payload returns the original result; a changed payload is rejected.
- Include committed operation state and the business mutation in the same supported PocketBase transaction. A pending operation after a crash is not reported as successful; reconcile deterministic record identities before retrying. Errors retain an observable failed/uncertain state.
- Use `withBatchLease` and supplier-scoped fencing for matching and receipt posting. Preserve preview freshness checks, source identities, posted-page protection and duplicate-document detection.
- Serialize PO header/item/verification edits through a shared resource lease used by both relevant app handlers and MCP operations. A lease used only by MCP would not prevent concurrent web edits.
- Require the PocketBase batch/locking schema when writes need it. If unavailable, fail closed with a setup error instead of falling back to unfenced writes.
- Do not describe multi-chunk PocketBase batches as atomic. Keep one business commit within the transaction's supported mutation count; reject oversized manual PO creations with a clear limit, or split them into explicitly observable draft operations before final verification.
- Long batch processing uses durable `mcp_jobs` records, with a create-based claim, ownership fence and bounded retries. A small worker in the MCP deployment runs `runStagedBatch` through the no-delete client; provider keys are optional for read tools but required for AI processing. Start the worker only when writes and required schema are configured.
- A disconnect does not enqueue the same job twice or start a second receipt posting. Workers resume status from PocketBase after a container restart. No job invokes `/api/dn-batches/tick` with the shared ingest secret or changes global production pipeline flags.
## Schema and runtime setup
Provide an idempotent migration for `mcp_operations`, `mcp_jobs` and MCP resource locks, including unique operation/resource keys and server-only access rules. Use a development/staging database to verify it before production application. Retain existing DN transaction/locking schema requirements. Audit records preserve before/after business snapshots or references without source file bytes or credentials.
Use local-only `.env.mcp.local` for developer credentials and `.env.mcp.example` for committed placeholders. Coolify supplies actual runtime variables. Provide token generation/hash instructions and separate read-only and write-enabled client examples for Codex, Hermes and Cowork. Avoid reusing the n8n ingest secret or exposing PocketBase administrator credentials to agents.
The MCP image must explicitly exclude `cooler.env`, `kilo.json`, `.env*` secrets, local agent settings, Git metadata and nested checkouts. Verify the build context and image contents; do not rely on the current dashboard `.dockerignore`, which does not explicitly exclude every known local secret file.
## Delivery sequence
1. Implement server factory, authenticated HTTP transport, health endpoint, isolated client policy, read-only registry and HTTP/stdio integration tests. This produces a runnable remote service locally without enabling writes.
2. Implement document references, downloads/resources and paginated purchasing detail reads. Prove protected document access and correct file bytes.
3. Add shared validated PO/supplier operations, journal/locks and app-route compatibility tests. Enable creation, updating and verification with no delete.
4. Add DN immutable upload/candidate/grouping/matching operations. Preserve revision and provenance rules.
5. Add preview processing, durable jobs, line receipt posting/corrections and retry/concurrency tests. Verify received/outstanding quantities after posting.
6. Package Coolify runtime and worker, finalize client settings/connection documentation, and run the full checks. Stage and review an end-to-end workflow before exposing production write credentials.
7. Deploy only after deployment access, host routing and actual runtime credentials are available. Confirm external HTTPS initialize/discovery, authenticated file access and allowed read/write scope. A prepared image/configuration is not reported as a deployed URL.
## Acceptance tests
- Existing stdio read tools remain callable; application read-only enforcement tests continue to pass without broad write-module exemptions.
- HTTP initialize/list/read succeed with a valid read key; absent/incorrect/revoked keys fail before database access.
- A read key cannot discover or invoke writes; malformed credential config prevents startup.
- Supplied untrusted Origin/Host is rejected; valid server clients can connect without Origin.
- Any attempted business record delete, batch delete, file removal or legacy promotion call fails without deleting anything. Infrastructure lock cleanup remains functional.
- Document download returns the expected filename/MIME/bytes for POs, source slips, batch sources/pages and candidate proof PDFs; missing documents and wrong references return meaningful errors. No response includes admin tokens or upstream signed URLs.
- Oversized/invalid uploads and arbitrary URLs/paths are rejected. Duplicate upload and operation retries create no second document/order/batch.
- Verified PO edits are blocked; cross-PO item updates and cross-site/supplier DN links are blocked. PO totals match the existing calculation helpers.
- Grouping respects assembly revisions, source page ownership and posted-page immutability.
- Stale comparison previews cannot post. Retried identical line posting returns the same receipt; simultaneous posting from two agents books once. Lost leases cannot commit business writes.
- A receipt write changes computed received/outstanding quantities exactly once and preserves its source proof/audit identity. The same database changes appear in the dashboard.
- Posted receipt corrections retain before/after history and follow the same restrictions as the app, including immutable line receipts.
- Interrupted processing resumes or reports a failed/uncertain operation honestly. AI failures retain the original files and do not imply a successful receipt.
- Run targeted new tests during development, then the full Vitest suite, TypeScript check, relevant lint and container/HTTP smoke checks before claiming completion.
- Live production checks remain read-only until the new deployment and a specific test write workflow have been reviewed. Use staging fixtures for the complete mutation/download/matching/posting exercise.
## Deployment readiness and remaining inputs
There is no Coolify management connector available in this session's tool inventory. Implementation, tests and deployment files can be completed locally; actual deployment may require access through the existing deployment workflow or the user's Coolify session. The proposed domain also needs DNS/routing access. Do not request that access before a runnable, tested service is ready to deploy.
The remaining product question is whether a separate stock-on-hand ledger is desired. It does not block remote access, document handling or PO receipt functionality and is not silently added to this implementation.