Skip to main content
Glama
J-X0
by J-X0

westmere-recsys

Recommendation engine for Meridian Commercial (Project Westmere). It ranks retail merchandising candidates with a Thompson-sampling bandit, and it does so under a hard operating constraint: spend is capped per tenant per calendar month.

The delivery is an MCP server: JSON-RPC 2.0 over newline-delimited stdio, implemented on the standard library (no mcp SDK required), exposing three tools: recommend, record_feedback, budget_status.

How the spend cap shapes the design

Every contextual score comes from a ScoringProvider (an LLM call in production). That call is the operating spend. The engine treats exploration as something it has to pay for:

  • Before scoring an arm, the engine tries to charge the provider's per-call cost against the tenant's monthly budget via an atomic check-and-charge ledger (SpendLedger.try_charge).

  • If the charge succeeds, it pays for a contextual prior and blends it with the Thompson sample. This is exploration.

  • If the charge is refused because the cap is reached, the engine serves that arm from the bandit's posterior mean at zero cost. This is exploit-only degradation.

Consequences, all covered by tests:

  • Recorded spend can never cross the cap (try_charge is atomic; no partial charges).

  • The service keeps returning recommendations after the budget is gone.

  • Every decision emits an AuditRecord (spend before/after, provider calls, chosen arms, which were explored, and what constraints rejected which arms).

Related MCP server: bank.mcp

Architecture

Requests flow through four layers, each with one job:

JSON-RPC stdio  ->  WestmereService   ->  RecommenderEngine  ->  ThompsonBandit
  (server.py)       validate/time/log     rank under budget       posterior draws
                                             |         |
                                       SpendLedger  ConstraintFilter
                                       (the cap)    (hard rejections)
                                             |
                                       ScoringProvider (stub | real)
  • server.py is transport only: parse a line, dispatch, write a line. Its dispatch function is pure so it can be tested without any I/O.

  • WestmereService is the boundary: nothing reaches the engine without being validated. It owns config wiring, timing, and structured logging.

  • RecommenderEngine is the algorithm: constraint filter, then per-arm charge-and-score, then rank. It never talks to the transport or the config.

  • SpendLedger, ConstraintFilter, ThompsonBandit and ScoringProvider are independent collaborators, each unit-tested in isolation.

Design decisions that had a real alternative are recorded in docs/adr/.

Layout

westmererecsys/
  domain.py        Arm, Context, Recommendation, AuditRecord, RecommendationResult
  bandit.py        ThompsonBandit (Beta-Bernoulli posterior, seedable RNG)
  budget.py        SpendLedger, per-tenant/per-month cap, BudgetExceeded
  constraints.py   ConstraintFilter + builtin constraints
  engine.py        RecommenderEngine: ties the above together
  config.py        Config: load/validate from dict, JSON file, or env
  errors.py        WestmereError / ConfigError / ValidationError
  logging_setup.py JSON-per-line structured logging to stderr
  service.py       WestmereService: input validation, timing, JSON results
  server.py        MCP JSON-RPC stdio entry point (dispatch + serve loop)
  providers/
    base.py        ScoringProvider interface (defines cost_cents)
    stub.py        StubScoringProvider: deterministic, offline
    real.py        RealScoringProvider: OpenAI-compatible, credential-gated
tests/

Setup and tests

make install     # create .venv and install the package with dev extras
make test        # run the suite (offline, no API key needed)
make lint        # byte-compile the package and tests

PY overrides the interpreter, e.g. make test PY=python3.

The entire suite runs on the deterministic stub provider with no network access and no API key.

Running the server

make run                                  # cap 1000c/tenant/month, stub provider
python3 -m westmererecsys.server --config config.example.json --catalogue catalogue.example.json
python3 -m westmererecsys.server --default-cap-cents 500 --log-level DEBUG

Flags: --config (JSON config file), --catalogue (JSON array of items used when a recommend call omits candidates), --default-cap-cents (used when no --config is given), --log-level. Environment overrides WESTMERE_* (e.g. WESTMERE_DEFAULT_CAP_CENTS, WESTMERE_PROVIDER, WESTMERE_SEED) are applied on top of the file. A missing or invalid config fails fast with exit code 2.

It speaks JSON-RPC 2.0, one message per line, on stdin/stdout. Example session:

{"jsonrpc":"2.0","id":1,"method":"initialize"}
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"recommend","arguments":{"tenant_id":"store-42","candidates":[{"id":"a"},{"id":"b"}],"k":1}}}

Config file keys

default_cap_cents (required), per_tenant_caps, provider (stub|real), provider_cost_cents, explore_weight, seed, prior_alpha, prior_beta, require_in_stock, exclude_categories, max_price_cents, max_candidates, max_k, log_level.

Failure handling and degradation

  • Bad request input (missing tenant_id, k out of range, malformed candidate, too many candidates) raises ValidationError, surfaced to the client as an MCP tool error (isError: true) rather than crashing the loop.

  • Missing/invalid config or catalogue files raise ConfigError at startup.

  • max_candidates / max_k bound per-request work so one call cannot exhaust resources.

  • If the provider raises mid-request, the engine refunds that arm's charge (the tenant is not billed for spend that produced nothing) and degrades the arm to posterior-mean scoring. The provider_errors count is in every audit record.

  • Once the monthly cap is reached, the engine serves exploit-only at zero cost and logs a warning; the service keeps responding.

Using the engine

from westmererecsys import (
    Arm, Context, RecommenderEngine, SpendLedger, ThompsonBandit,
    ConstraintFilter, require_in_stock,
)
from westmererecsys.providers.stub import StubScoringProvider

engine = RecommenderEngine(
    provider=StubScoringProvider(cost_cents=2),
    ledger=SpendLedger(default_cap_cents=500, per_tenant_caps={"vip": 5000}),
    bandit=ThompsonBandit(seed=7),
    constraints=ConstraintFilter([require_in_stock()]),
)

ctx = Context(tenant_id="store-42", features={"segment": "loyal"})
catalogue = [Arm(f"sku-{i}", f"Item {i}", "apparel") for i in range(20)]

result = engine.recommend(ctx, catalogue, k=5)
for rec in result.recommendations:
    print(rec.arm_id, round(rec.score, 3), "explored" if rec.explored else "exploit")

# Fold observed outcomes back in (reward in [0, 1]):
engine.record_feedback("sku-3", reward=1.0)

Production provider

RealScoringProvider calls an OpenAI-compatible chat endpoint. It reads WESTMERE_LLM_API_KEY and optional WESTMERE_LLM_BASE_URL, and it raises ProviderUnavailable rather than silently degrading when credentials are missing, httpx is not installed, or the response cannot be parsed. Install its dependency with pip install .[real].

Known limitations

  • State is in-memory. Bandit posteriors and the spend ledger live in the server process. Restarting resets learning and, more importantly, resets spend to zero for the month. ThompsonBandit.export_state/load_state exist as the persistence seam, but the ledger has no durable store yet. A restart loop could let a tenant exceed the intended monthly cap. See docs/adr/0005-in-memory-state.md.

  • Single process, no concurrency control. The stdio loop handles one request at a time. SpendLedger.try_charge is atomic within a process but there is no cross-process locking, so running multiple servers against one tenant would not share a budget.

  • Month boundaries use the server clock in UTC. A tenant billed in another timezone may see the cap reset a few hours early or late.

  • The blend weight is static. explore_weight is a fixed constant, not tuned per tenant or decayed over time.

  • RealScoringProvider is not covered by the test suite (it needs live credentials). Its parsing and credential-guard logic are tested; the network call itself is not.


Meridian Commercial is an illustrative client; this repository is a self-directed reference implementation built to work end to end.

Available Tools

3 tools
budget_statusC

Report a tenant's monthly spend and remaining cap.

ParametersJSON Schema
NameRequiredDescriptionDefault
monthNo
tenant_idYes

TDQS

C2.7/5.0
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries the full burden of behavioral disclosure. It implies a read-only report and mentions the output concepts (spend and remaining cap), but it does not explain month defaults, response format, error behavior, or access requirements.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is one concise sentence with no filler, front-loading the key action and subject. It is easy to parse, though the brevity contributes to missing details elsewhere.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

There is no output schema and no annotations, so the description needs to explain return values and behavior more fully. It does not describe what the report looks like, what happens if no month is provided, or any edge cases, leaving the agent under-informed.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters2/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, so the description must compensate for the lack of parameter documentation. It hints that tenant_id identifies the tenant and month selects the month, but it does not clarify formats, the optionality of month, or the exact meaning of 'remaining cap'.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description uses a specific verb ('Report') and a clear resource ('a tenant's monthly spend and remaining cap'), so an agent can understand the core function. It does not explicitly distinguish from sibling tools, but the resource is distinct enough from 'recommend' and 'record_feedback'.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

No guidance is given on when to use this tool versus the sibling tools, nor any context about prerequisites or typical scenarios. The description only states what the tool does, leaving the agent to infer when it should be invoked.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

recommendC

Rank candidate items for a tenant under the monthly spend cap.

ParametersJSON Schema
NameRequiredDescriptionDefault
kNo
featuresNo
tenant_idYes
candidatesNo

TDQS

C2.9/5.0
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries the full burden. It does not disclose whether ranking has side effects, how the spend cap is enforced, what happens when the cap is exceeded, or any rate/ordering behavior. The behavioral profile is largely unknown.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, well-structured sentence with no filler. It front-loads the core purpose, though its brevity leaves key behavioral and parameter details uncovered.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a tool with 4 parameters, nested objects, no output schema, and no annotations, this description is too sparse. It lacks return-value expectations, parameter semantics for k and features, and any note on how ranking output is structured.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters2/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

With 0% schema description coverage and 4 parameters, the description only implicitly maps to tenant_id and candidates, while k and features are completely unaddressed. It does not compensate for the schema gap.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a clear action — 'Rank candidate items' — with a specific resource ('a tenant') and a constraint ('monthly spend cap'). It is distinguishable from sibling tools like record_feedback and budget_status, though it does not explicitly name them.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The phrase 'for a tenant under the monthly spend cap' implies the context in which this tool is appropriate, but there is no explicit when-to-use or when-not-to-use guidance, and no alternative tools are named.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

record_feedbackC

Record an observed reward in [0,1] for an arm.

ParametersJSON Schema
NameRequiredDescriptionDefault
arm_idYes
rewardYes

TDQS

C2.9/5.0
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries the full burden of behavioral disclosure. It adds the reward range [0,1], which is useful, but it does not state whether the operation is append-only, idempotent, requires an existing arm, or has any side effects on the recommendation model. For a write operation, this is a significant gap.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

A single sentence that is front-loaded with the verb and includes the essential constraint. Every word adds meaning; there is no fluff or redundancy.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a tool with no annotations, no output schema, and no usage guidance, the description is too sparse. It defines the basic operation but leaves out the surrounding workflow, return behavior, and any consequences of calling it, which are needed for reliable invocation in context.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, so the description must explain the parameters. It conveys that arm_id refers to an arm and reward is a value in [0,1], covering both parameters at a basic level. However, it lacks details about how arm_id is obtained or what happens with out-of-range rewards.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the action ('Record'), the object ('an observed reward'), and the target ('for an arm'), making the tool's function immediately obvious. It does not explicitly differentiate from siblings, but the contrast with 'recommend' and 'budget_status' is conceptually clear.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description gives no guidance on when to use this tool versus the siblings. It does not mention that it should follow a 'recommend' call, nor does it explain the workflow context, leaving the agent to infer the appropriate usage.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections. Dates show when Glama detected each change.

  1. 3 tool updatesv0.1.0
    • First observedbudget_status
    • First observedrecommend
    • First observedrecord_feedback

TDQS

B3.2/5.0
Disambiguation5/5

Each tool has a clearly distinct purpose: recommend generates candidate rankings under budget, record_feedback logs observed rewards, and budget_status reports spend/remaining cap. There is no meaningful overlap between them.

Naming Consistency3/5

The names are readable but not fully consistent: 'record_feedback' follows a verb_noun pattern, 'recommend' is a bare verb, and 'budget_status' is noun_noun without an action verb. A consistent set like 'recommend_items', 'record_feedback', and 'get_budget_status' would improve predictability.

Tool Count5/5

Three tools is a well-scoped size for a focused recommendation/bandit service. Each tool covers a necessary part of the core workflow: recommending, recording feedback, and checking budget.

Completeness4/5

The core loop of recommend -> record_feedback -> check budget is covered, and there are no dead ends in that workflow. However, there is no tool for managing tenants, candidate items, or budget configuration, which are minor gaps if the server is expected to handle those resources.

Maintenance

ActivityInactive
ResponsivenessNo issues

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    B
    maintenance
    MCP server that turns bank transactions into a financial digest including cash-flow forecast, spending breakdown, fee detection, and receipt reconciliation, exposing deterministic engines as JSON-RPC tools.
    Apache 2.0
  • A
    license
    Not graded
    quality
    B
    maintenance
    Remote MCP server for auditing app surfaces in AI agent ecosystems, checking recommendation status, and creating distribution plans.
    16
    MIT

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/J-X0/meridian-commercial-recsys-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server