Skip to main content
Glama
Legacy Billing MCP Server
by besimademi

legacy-mcp-bridge

A small, complete example of making a legacy system agent-operable:

  • an untyped 2014-era billing module (legacy/billing.js) is wrapped, not rewritten

  • a typed anti-corruption layer and hexagonal ports isolate it

  • a modern replacement is introduced with the Strangler Fig pattern (legacy → shadow → modern)

  • characterization tests pin the legacy behaviour, quirks included, so refactoring can't change invoices

  • a Model Context Protocol (MCP) server exposes the system to AI agents as typed, validated, read-only tools

  • an agent context layer (AGENTS.md, .claude/commands) tells agents the rules of the codebase

Architecture

            AI agent (Claude, Cursor, ...)
                     │  MCP (stdio, JSON-RPC)
            ┌────────▼─────────┐
            │  src/mcp/server  │  zod input/output contracts, tool errors
            └────────┬─────────┘
                     │ InvoiceCalculator port
        ┌────────────▼─────────────┐
        │ StranglerInvoiceCalculator│  MIGRATION_MODE = legacy | shadow | modern
        └──────┬────────────┬──────┘
               │            │
   LegacyInvoiceCalculator  ModernInvoiceCalculator
   (anti-corruption layer)  (pure, typed, injected)
               │
        legacy/billing.js  ← frozen, never edited

Layer

File

Responsibility

Contracts

src/domain/invoice.ts

zod schemas = single source of truth for types and MCP tool schemas

Ports

src/domain/ports.ts

interfaces the core depends on

Legacy adapter

src/adapters/legacy-billing.ts

translates untyped legacy data and error codes into domain types

Modern adapter

src/adapters/modern-calculator.ts

clean reimplementation, named constants, no globals

Strangler facade

src/strangler.ts

routes traffic; in shadow mode runs both and logs mismatches

Composition root

src/container.ts

the only place that wires concrete classes (DI)

MCP adapter

src/mcp/server.ts

tools for agents

Related MCP server: Ops MCP Server (Finamatik)

MCP tools

Tool

What it does

list_customers

all customers and their pricing tier

get_customer

one customer by id

calculate_invoice

subtotal, tier discount, 18% VAT, shipping, total, through the strangler facade

compare_implementations

runs legacy and modern on the same order and reports whether they match

Every tool has an input and output schema and readOnlyHint. Unknown customers return a tool error that tells the agent how to recover (Call list_customers to see valid ids) instead of crashing.

Pinned legacy quirks

Characterization tests document behaviour that is surprising but that customers' invoices depend on. They are kept until the business decides otherwise:

  • tier codes are case-sensitive: customer 4 has tier g, so gets no gold discount

  • negative quantities are silently treated as 0

  • the bulk discount (2% at qty ≥ 10) is applied per line, before rounding

  • the subtotal is a raw float sum and is not rounded

  • shipping is not taxed; it is free above 1000 after discount, and 0 for empty orders

A seeded property test also checks that the modern calculator matches legacy exactly on 5,000 random orders.

Run it

npm install
npm test          # characterization + MCP contract tests
npm run build

Add it to an MCP client (for example Claude Code, Claude Desktop or Cursor):

{
  "mcpServers": {
    "legacy-billing": {
      "command": "node",
      "args": ["/absolute/path/to/legacy-mcp-bridge/dist/src/index.js"],
      "env": { "MIGRATION_MODE": "shadow" }
    }
  }
}

Or with Claude Code:

claude mcp add legacy-billing --env MIGRATION_MODE=shadow -- node dist/src/index.js

Migration playbook

  1. legacy: wrap the old module behind a port; write characterization tests against it.

  2. shadow: build the modern implementation; run both on every call, serve legacy, log mismatches.

  3. modern: when shadow logs are clean and the tests pass, switch the flag.

  4. Delete the legacy adapter and module once nothing routes to them.

License

MIT

Available Tools

4 tools
calculate_invoiceCalculate invoiceB
Read-onlyIdempotent

Calculate subtotal, tier discount, 18% VAT, shipping and total for an order. Routed through the strangler facade (legacy, shadow or modern implementation).

NameRequiredDescriptionDefault
itemsYesOrder lines
customerIdYesCustomer id from list_customers

Output Schema

NameRequiredDescription
taxYes
totalYes
discountYes
shippingYes
subtotalYes

TDQS

B3.2/5.0
Behavior3/5

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

Annotations already declare readOnlyHint and idempotentHint true, so the safety profile is covered. The description adds genuine value by disclosing the calculation rules (18% VAT, tier discount, shipping), but the routing sentence leaves the agent unable to tell how the legacy/shadow/modern implementation is selected or whether the result could vary by route. Rounding and currency handling are also unstated.

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?

Two tight sentences, with the computed components front-loaded so the agent learns the output shape immediately. The trailing parenthetical about the strangler facade is slightly cryptic and could have been clearer or cut, but nothing is padded.

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

Completeness3/5

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

With an output schema present and full parameter coverage, the description only needs to explain the domain logic, which it largely does via the VAT/discount/shipping list. The gap is the unexplained routing across legacy, shadow and modern implementations: an agent cannot know which one it hit or whether results should be compared, which is material for a money-calculation tool.

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 100%, and the schema itself documents customerId, items, sku, price in EUR, and the legacy negative-quantity behaviour. The description adds nothing about parameter meaning beyond that, so the baseline of 3 applies.

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 names a specific verb (calculate) and an explicit output set: subtotal, tier discount, 18% VAT, shipping, total for an order. That is far more than a restatement of the name. It does not, however, differentiate itself from siblings such as compare_implementations, which the mention of legacy/shadow/modern facades implicitly gestures at.

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?

There is no statement of when to call this rather than compare_implementations, nor any prerequisite or exclusion. The 'strangler facade' sentence hints that an implementation is chosen somewhere, but it never tells the agent whether this call is the right one for comparing behaviour across implementations or is merely the production path.

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

compare_implementationsCompare legacy vs modernA
Read-onlyIdempotent

Run the order through BOTH the legacy and the modern calculator and report whether they agree. Use this to verify a refactor before switching MIGRATION_MODE to modern.

NameRequiredDescriptionDefault
itemsYesOrder lines
customerIdYesCustomer id from list_customers

Output Schema

NameRequiredDescription
matchYes
legacyYes
modernYes

TDQS

A4.3/5.0
Behavior4/5

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

Annotations already declare readOnlyHint and idempotentHint, so safety is covered. The description adds genuinely useful context beyond that: the tool executes two independent calculation paths and returns an agreement verdict, which explains cost and result shape. It stops short of saying what happens on mismatch (error vs. report body), but the output schema covers the response.

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?

Two short sentences, front-loaded with the core behavior and followed by the operational trigger. No redundant restatement of the name or schema.

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

Completeness5/5

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

For a two-parameter read-only comparison tool with a fully documented schema, annotations, and an output schema, nothing essential is missing. The description supplies the purpose, the dual-execution semantics, and the migration-workflow context an agent needs to choose it correctly.

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 100%, including the legacy negative-quantity behavior and the customerId source (list_customers). The description adds no parameter-level meaning, so the baseline of 3 applies.

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

Purpose5/5

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

States a specific verb (compare/run) and resource (order through legacy and modern calculators) and the output form (whether they agree). It is clearly distinguishable from the sibling calculate_invoice, which computes a single result rather than diffing two implementations.

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

Usage Guidelines4/5

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

Explicitly says when to use it: verifying a refactor before flipping MIGRATION_MODE to modern. It does not state when not to use it (e.g., for production invoicing use calculate_invoice), so it is a clear context rather than a full routing rule.

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

get_customerGet customerB
Read-only

Get one customer by id.

NameRequiredDescriptionDefault
customerIdYesCustomer id from list_customers

Output Schema

NameRequiredDescription
idYes
nameYes
tierYes
countryYes

TDQS

B3.3/5.0
Behavior2/5

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

Annotations already declare readOnlyHint=true, so the safety profile is covered. Beyond that the description adds nothing: it does not say what happens when the id is unknown (null vs error), whether the lookup is cached, or any permission requirement. For a tool whose annotations already carry the read-only signal, the description contributes essentially no extra behavioral context.

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?

One short, front-loaded sentence with a verb, a resource, and the lookup qualifier. There is no filler and nothing that would need to be moved earlier.

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

Completeness3/5

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

This is a simple single-parameter read with an output schema, so much of the burden is already carried by structured fields. Still, a complete definition would say how a missing or invalid id is surfaced, which matters for an agent deciding whether to call this or list_customers.

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 100%: the single customerId parameter is documented in the schema with its origin ("Customer id from list_customers"), including type and bounds. The description adds no format, range, or sourcing detail beyond that, so the baseline 3 applies.

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 specific verb and resource ("Get one customer") and the lookup key ("by id"), which implicitly distinguishes it from the sibling list_customers. It stops short of explicitly naming the sibling the way a 5 would.

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 "by id" implies you must already have a customer id, and the sibling name list_customers is only hinted at via the schema's "Customer id from list_customers". There is no explicit when-to-use-this-vs-alternatives guidance or mention of what to do without an id.

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

list_customersList customersA
Read-only

List all customers with their pricing tier.

NameRequiredDescriptionDefault

No parameters

Output Schema

NameRequiredDescription
customersYes

TDQS

A3.7/5.0
Behavior3/5

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

The readOnlyHint annotation already establishes that this is a safe read, so the description only needs to add context. It discloses that pricing tier is included in the results, but says nothing about ordering, pagination, or volume limits for a full customer list.

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 front-loaded sentence with no redundant or filler wording. Every element, the listing action and the included pricing tier, earns its place.

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

Completeness4/5

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

With no input parameters, a read-only annotation, and an output schema covering return values, the description is close to sufficient. The only gap is operational context such as pagination or ordering behavior for a potentially large customer list.

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

Parameters4/5

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

The tool takes zero parameters, so the baseline is 4 and there is no parameter meaning the description could add. Nothing in the schema is left unexplained.

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 states a specific verb and resource ('List all customers') and adds the returned attribute ('their pricing tier'), so the operation is unambiguous. It does not distinguish itself from the sibling get_customer, which is the only missing piece for a 5.

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 word 'all' implies a bulk listing as opposed to fetching one customer, which hints at when to use it over get_customer. However, no alternative is named and no condition or exclusion is stated, leaving usage to inference.

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.

  1. 4 tool updatesv0.1.0
    • First observedcalculate_invoice
    • First observedcompare_implementations
    • First observedget_customer
    • First observedlist_customers

TDQS

A3.5/5.0

Scored across 4 tools

Disambiguation4/5

The two customer tools (list vs get-by-id) are clearly separated, and calculate_invoice vs compare_implementations have distinct purposes spelled out in their descriptions. There is minor conceptual overlap since compare_implementations implicitly runs both calculators, so an agent might pick calculate_invoice when it really wanted verification, but the descriptions mitigate this.

Naming Consistency5/5

All four tools follow a clean snake_case verb_noun pattern (list_customers, get_customer, calculate_invoice, compare_implementations). The verbs are conventional and predictable for their actions.

Tool Count3/5

Four tools is thin for a server labeled 'Billing'; the surface is essentially read-customers plus invoice math plus a migration check. The count is defensible for a narrow strangler-facade migration service, but feels borderline minimal for the stated domain.

Completeness3/5

Customers are read-only (no create/update) and there is no invoice persistence or retrieval — only calculation — plus no order or payment operations. The calculation and migration-verification path is well covered, but the broader billing lifecycle has notable gaps.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    D
    maintenance
    Exposes a ledger system (invoice queue, duplicate control, VAT register, contractor history, decision journal) as MCP tools for AI agents, enabling accurate invoice processing with deterministic validation.
    7
    MIT
  • A
    license
    A
    quality
    A
    maintenance
    Ten typed tools that let Claude Desktop, Claude Code or any MCP client work with a company operations dataset: customers, orders, invoices, payments and stock. Every call is validated against a schema and recorded in a log. Python, official mcp SDK with FastMCP, MIT licence.
    10
    10 PyPI
    MIT
  • A
    license
    A
    quality
    C
    maintenance
    Lets an AI agent list reports, start and poll exports, and look up insurance claims from a legacy system that has no API, with each server process serving only the tools and report types its role permits. Every tool call is written to an append-only, field-masked audit log, and the backend's login credentials stay in an encrypted vault the model never sees.
    3
    MIT
  • A
    license
    A
    quality
    B
    maintenance
    Enables MCP clients such as Claude and Cursor to manage customers, create and update draft invoices, list or download invoices as PDF and XML e-invoices, and record and list payments in a BillingEngine account. It runs locally with your API key and never deletes data.
    10
    338 npm
    MIT