@posara/horizon-mcp
v1.1.2
Published
POSARA Horizon MCP Server — connect any AI assistant to your business data
Readme
POSARA Horizon MCP Server
Connects any MCP-capable AI assistant (Claude Desktop, Claude Code, …) to a POSARA Horizon business: read reports, manage the catalog, control inventory, and raise purchase orders.
Quick Start
- In Horizon, open Apps → Horizon MCP → Tokens and generate a token, ticking only the privileges the assistant should have.
- Run the server with
npx, pointing it at your instance:
HORIZON_API_URL=https://posara-horizon-t1.dcn01.ctdn.dev \
HORIZON_API_KEY=your-mcp-token \
npx @posara/horizon-mcpThis starts the HTTP transport on :3001/mcp by default. For Claude Desktop
or other local stdio clients, set MCP_TRANSPORT=stdio instead:
{
"mcpServers": {
"posara-horizon": {
"command": "npx",
"args": ["-y", "@posara/horizon-mcp"],
"env": {
"MCP_TRANSPORT": "stdio",
"HORIZON_API_URL": "https://posara-horizon-t1.dcn01.ctdn.dev",
"HORIZON_API_KEY": "your-mcp-token"
}
}
}
}Pass --verbose (or set MCP_LOG_LEVEL=verbose) to log each request and tool
call to stderr:
npx @posara/horizon-mcp --verboseAlready on Horizon? You may not need to run anything
Every Horizon deployment already exposes MCP over HTTP at /api/v1/mcp —
remote clients (Claude custom connectors, any other remote MCP client) can
talk to it directly instead of running this package at all:
POST https://posara-horizon-t1.dcn01.ctdn.dev/api/v1/mcp
Authorization: Bearer <mcp-token>Run this package yourself only for local stdio access (Claude Desktop) or to front a self-hosted instance. One deployment serves many businesses — the token identifies both the business and the granted privileges.
Claude custom connector (OAuth)
Instead of pasting a token, register an OAuth client in Apps → Horizon MCP →
Connectors, then in Claude: Settings → Connectors → Add custom connector,
paste https://<your-host>/api/v1/mcp, and put the Client ID and Client Secret
under Advanced settings. Claude opens the Horizon consent screen; each user
approves individually, and access tokens refresh automatically.
Discovery is served by the app, not this package:
| Endpoint | Purpose |
|---|---|
| /.well-known/oauth-protected-resource[/api/v1/mcp] | RFC 9728 — points clients at the authorization server |
| /.well-known/oauth-authorization-server[/api/v1/mcp] | RFC 8414 — authorize/token endpoints, scopes, PKCE methods |
| /oauth/authorize | consent screen |
| /api/v1/oauth/token | token exchange (client_secret_post or client_secret_basic) |
Unauthenticated or expired requests get 401 with a
WWW-Authenticate: Bearer … resource_metadata="…" challenge, which is what
triggers the client to (re-)run the flow. Set HORIZON_PUBLIC_URL when
HORIZON_API_URL is an internal address, so that challenge points somewhere
clients can actually reach.
There is deliberately no Dynamic Client Registration — registration_endpoint
is absent from the metadata, so clients fall back to asking for a Client ID and
Secret. A business registers a connector once, on purpose.
Tools
| Area | Tools |
|---|---|
| Sales & reporting | get_dashboard_overview · get_sales_summary · get_top_products · get_invoices · get_invoice · get_customers · get_branches |
| Catalog | search_products · get_product · create_product* · update_product* · get_product_activity · get_catalog_metadata |
| Bundles | get_product_bundle · set_product_bundle* |
| Inventory | get_inventory_status · get_inventory_movements · adjust_inventory* · import_stock* · get_inventory_summary · get_inventory_analytics · get_expiring_inventory · get_warehouses |
| Purchasing | get_suppliers · get_reorder_suggestions · get_purchase_orders · get_purchase_order · create_purchase_order* · update_purchase_order* · receive_purchase_order* · get_purchase_order_receipts |
* Write tool — needs the matching *.write privilege on the token and is
annotated readOnlyHint: false so clients can prompt before running it.
Restocking flow
get_reorder_suggestions scans stock against each product's reorder point and
proposes quantities. Feed the result to create_purchase_order, then
receive_purchase_order when the goods arrive — that last step is what actually
raises stock levels and updates product cost. For stock that never had a PO
(opening balances, a walk-in supplier drop-off), use import_stock instead —
it optionally links a supplier and records a payment against it.
Both receive_purchase_order and import_stock (and adjust_inventory for
inbound/outbound corrections) enforce the product's tracking settings: check
get_product's identifierTrackingMode ("imei" / "serial") and
expiryTracking first. If tracking is on, the matching identifiers /
expiryDate fields are mandatory and the identifier count must equal the
quantity converted to base units (via the line's unit conversion rate) — the
call is rejected otherwise.
Audit trail
Every tool call is posted back to Horizon and stored with its arguments, outcome, and latency. Browse it in Apps → Horizon MCP → Call History. Logging is fire-and-forget: if the endpoint is unreachable, the tool still returns normally.
Privileges
Tokens carry Horizon API scopes. Typical sets:
- Read-only analyst —
reports.read,products.read,inventory.read,invoices.read,customers.read,branches.read - Inventory manager — the above plus
inventory.write,products.write,warehouses.read - Purchasing — plus
suppliers.read,purchase_orders.read,purchase_orders.write
A tool called without the required scope fails with a clear 403 naming the
missing scope, rather than silently returning nothing.
Contributing
Building from source — not needed just to use the published package:
bun install
cp .env.example .env # set HORIZON_API_URL
bun run dev # HTTP transport on :3001/mcp
bun run smoke # asserts all tools register (no network calls)Local stdio config, running from source instead of the published package:
{
"mcpServers": {
"posara-horizon": {
"command": "bun",
"args": ["run", "/path/to/packages/horizon-mcp/src/index.ts"],
"env": {
"MCP_TRANSPORT": "stdio",
"HORIZON_API_URL": "https://posara-horizon-t1.dcn01.ctdn.dev",
"HORIZON_API_KEY": "your-mcp-token"
}
}
}
}Docker: docker-compose.yml at the repo root defines the horizon-mcp
service, talking to the app over the compose network
(HORIZON_API_URL=http://posara-horizon-app:3000).
docker compose up -d horizon-mcp