Skip to main content
Glama
Mmarucr7

customer-refund-mcp-server

by Mmarucr7

customer-refund-mcp-server

A Model Context Protocol (MCP) server that exposes two tools over the stdio transport:

  • get_customer_record returns a customer record by ID.

  • trigger_refund issues a refund to a customer and reduces their refundable balance.

Every tool argument is validated with a strict schema before any business logic runs. Invalid input is rejected with a JSON-RPC protocol error, business-rule failures are returned as tool execution errors, and stdout carries nothing except JSON-RPC messages.

The server ships with an in-memory customer store and a small set of seed customers. The store sits behind a CustomerRepository interface so it can be replaced with a real data store.

Requirements

  • Node.js 20.19 or later (Node.js 22 is recommended; see .nvmrc)

  • npm

Related MCP server: customer-mcp

Install and build

git clone https://github.com/Mmarucr7/customer-refund-mcp-server.git
cd customer-refund-mcp-server
npm ci
npm run build

The build writes the server to dist/index.js.

Running the server

node dist/index.js

The server reads JSON-RPC messages from stdin, writes responses to stdout and writes logs to stderr. It does not print anything to stdout until a client sends a request.

Set LOG_LEVEL to debug, info, warn or error to control log output. The default is info, and unrecognized values fall back to info.

LOG_LEVEL=debug node dist/index.js

The server shuts down cleanly with exit code 0 on SIGINT, SIGTERM or when stdin closes. An uncaught exception or unhandled promise rejection is logged to stderr and the process exits with code 1.

MCP client configuration

Point the client at node and the absolute path of the built entry point. Do not use npm start, npx or tsx in a client configuration: npm and other wrappers print their own lines to stdout, which corrupts the JSON-RPC stream.

Claude Desktop

Add the server to claude_desktop_config.json:

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json

  • Windows: %APPDATA%\Claude\claude_desktop_config.json

{
  "mcpServers": {
    "customer-refund": {
      "command": "node",
      "args": ["/absolute/path/to/customer-refund-mcp-server/dist/index.js"],
      "env": {
        "LOG_LEVEL": "info"
      }
    }
  }
}

On Windows, use an escaped path such as "C:\\Users\\you\\customer-refund-mcp-server\\dist\\index.js". Restart the client after editing the file. Any other MCP client that launches stdio servers takes the same command and args.

Tool reference

get_customer_record

Returns the customer record for a customer ID.

Input:

Field

Type

Rules

customer_id

string

Must match ^CUST-\d{5}$

Output (structuredContent, also serialized as JSON in a text content block):

Field

Type

Description

customerId

string

Customer ID

name

string

Customer name

email

string

Customer email address

status

string

active or suspended

currency

string

ISO 4217 currency code

refundableBalance

number

Amount that can still be refunded

createdAt

string

Account creation time (ISO 8601)

Example result:

{
  "content": [{ "type": "text", "text": "{\"customerId\":\"CUST-00001\", ...}" }],
  "structuredContent": {
    "customerId": "CUST-00001",
    "name": "Alice Johnson",
    "email": "alice.johnson@example.com",
    "status": "active",
    "currency": "USD",
    "refundableBalance": 500,
    "createdAt": "2024-01-15T09:30:00.000Z"
  }
}

trigger_refund

Issues a refund to an active customer and reduces their refundable balance.

Input:

Field

Type

Rules

customer_id

string

Must match ^CUST-\d{5}$

amount

number

Greater than 0, at most 1,000,000, at most 2 decimal places. Strings are not accepted.

reason

string

10 to 500 characters after trimming leading and trailing whitespace

Output (structuredContent, also serialized as JSON in a text content block):

Field

Type

Description

refundId

string

Random UUID for the refund

customerId

string

Customer ID

amount

number

Refunded amount

currency

string

Currency of the customer account

reason

string

Trimmed reason

status

string

Always processed

processedAt

string

Processing time (ISO 8601)

remainingRefundableBalance

number

Refundable balance after this refund

All money arithmetic is done in integer cents, so repeated refunds do not accumulate floating-point error. Concurrent refunds against the same customer cannot overdraw the balance: the balance update only succeeds if the balance has not changed since it was read, and otherwise the refund is re-evaluated against the new balance.

Seed data

Customer ID

Status

Refundable balance

CUST-00001

active

500.00 USD

CUST-00002

suspended

200.00 USD

CUST-00003

active

0.00 USD

The store is in memory, so balances reset every time the server starts.

Validation rules

Arguments are validated with Zod. Every input object is strict and no value is coerced from one type to another.

  • The arguments object must be present and must be a JSON object. null, arrays and other types are rejected.

  • Properties that are not part of the schema are rejected.

  • customer_id must be exactly CUST- followed by five digits. Lowercase prefixes, other digit counts, empty strings and surrounding whitespace are rejected.

  • amount must be a JSON number. Numeric strings such as "10.00" are rejected, as are 0, negative numbers, values above 1,000,000 and values with more than two decimal places such as 10.001.

  • reason is trimmed before its length is checked, so a reason made only of whitespace is rejected.

The inputSchema and outputSchema published by tools/list are generated from the same Zod schemas that the server enforces, so the advertised schemas cannot drift from the enforced ones. Both include "additionalProperties": false. Two rules cannot be expressed in JSON Schema and are only enforced by the server: the two-decimal-place limit on amount and the trimming of reason before its length is checked.

Error handling

Situation

Response

Code

Invalid, missing or extra tool arguments

JSON-RPC error

-32602

Unknown tool name

JSON-RPC error

-32602

Unknown method

JSON-RPC error (SDK)

-32601

Unexpected internal failure

JSON-RPC error

-32603

Customer not found, account not active, amount exceeds the balance

Tool result with isError

none

Invalid JSON or a message that is not JSON-RPC

Logged to stderr, no response

none

Argument validation errors carry the individual problems in error.data:

{
  "jsonrpc": "2.0",
  "id": 2,
  "error": {
    "code": -32602,
    "message": "MCP error -32602: Invalid arguments for tool trigger_refund",
    "data": {
      "issues": [
        {
          "path": ["amount"],
          "code": "custom",
          "message": "amount must have at most 2 decimal places"
        }
      ]
    }
  }
}

Internal failures return the generic message Internal error. The underlying error, including its stack trace, is written to stderr and never sent to the client.

Business-rule failures are valid requests that the server cannot carry out. They are returned as normal tool results so the calling model can read the explanation and react to it:

{
  "content": [
    {
      "type": "text",
      "text": "Customer CUST-00002 is suspended. Refunds can only be issued to active accounts."
    }
  ],
  "isError": true
}

The MCP specification recommends reporting input-validation failures as tool execution errors, that is, as a tool result with isError: true, so that a model can see what went wrong and correct its arguments. This server deliberately departs from that recommendation and rejects invalid arguments with JSON-RPC error -32602 (Invalid params). Refunds move money, and a strict contract is easier to reason about: a call with invalid arguments never reaches the tool at all, and clients, gateways and audit logs can tell a rejected request apart from a request that ran and failed. Business-rule failures still follow the specification and are reported as tool execution errors.

Notes on SDK behavior

Some responses are produced by the MCP TypeScript SDK rather than by this server:

  • The SDK prefixes error messages with MCP error <code>: , so the unknown-tool message on the wire is MCP error -32602: Unknown tool: <name>.

  • When arguments is present but is not a JSON object (for example null or an array), the SDK rejects the request with -32602 before the tool schema runs. That response has no error.data.

  • When a line on stdin is not valid JSON, or is valid JSON but not a JSON-RPC message, the SDK stdio transport reports it to the server's error handler and does not send a response, so -32700 (Parse error) and -32600 (Invalid Request) are never sent. The server logs the problem to stderr and keeps running.

  • When a request's params do not match the shape the SDK expects for that method (for example a tools/call whose name is not a string), the SDK responds with -32603 and its own validation message.

Stdio isolation

On the stdio transport, stdout is the protocol channel. A single stray line on stdout breaks the client's JSON-RPC parser, so the server keeps stdout reserved for JSON-RPC messages:

  • All logging goes through a small structured logger that writes one JSON object per line to stderr.

  • The entry point rebinds console.log, console.info and console.debug to stderr before it loads any other module. Modules are loaded with dynamic import() after the rebinding, so even code that runs while a module is being loaded cannot write to stdout.

  • ESLint enforces no-console and forbids process.stdout throughout the codebase. The only exception is the console rebinding in src/index.ts.

  • The integration tests spawn the built server and fail if any stdout line is not a JSON-RPC 2.0 message, including after malformed input and after code calls console.log.

  • Client configurations run node dist/index.js directly, because npm and other wrappers write their own output to stdout.

Development

Script

Description

npm run build

Compile TypeScript to dist/

npm start

Run the built server (for local use only, not in client configs)

npm run dev

Run the server from source with tsx

npm run typecheck

Type-check sources and tests

npm run lint

Run ESLint with zero warnings allowed

npm run format

Format files with Prettier

npm run format:check

Check formatting

npm test

Build, then run unit and integration tests

npm run test:coverage

Build, then run tests with coverage thresholds

npm run inspector

Open the MCP Inspector against dist/index.js

The integration tests start dist/index.js as a child process, so npm test builds first. The MCP Inspector requires Node.js 22.19 or later. See TESTING.md for manual test cases and Inspector instructions.

Project layout:

src/
  index.ts                     entry point: stdout guard, transport, signal handling
  server.ts                    server factory, request handlers, error mapping
  logger.ts                    stderr-only structured logger
  schemas.ts                   Zod schemas and JSON Schema export
  errors.ts                    Zod-to-McpError conversion
  money.ts                     integer-cent conversions
  tools/tool.ts                tool contract and result builders
  tools/getCustomerRecord.ts   get_customer_record
  tools/triggerRefund.ts       trigger_refund
  data/customerRepository.ts   repository interface and in-memory store
test/
  unit/                        schema, tool, logger and in-process server tests
  integration/                 tests against the built server over stdio

License

MIT

Available Tools

2 tools
get_customer_recordB

Returns the customer record for a customer ID in the format CUST-NNNNN, including account status and refundable balance.

NameRequiredDescriptionDefault
customer_idYes

Output Schema

NameRequiredDescription
nameYes
emailYes
statusYes
currencyYes
createdAtYes
customerIdYes
refundableBalanceYes

TDQS

B3.3/5.0
Behavior3/5

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

With no annotations, the description carries the burden; "Returns" implies a read-only lookup, and it names the salient returned fields, which is useful. It does not disclose auth/permission requirements or what happens for an unknown yet well-formed ID, so behavioral disclosure is only partial.

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 front-loads the action and resource, then appends the useful payload detail. Nothing is padded or redundant.

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?

The tool is simple (one required param, output schema present so return values needn't be spelled out), and the description covers purpose, ID format, and key fields. It omits error/not-found behavior and any routing hint relative to trigger_refund, which for a refund-adjacent lookup leaves a real gap.

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 coverage is 0%, so the description must compensate, and it does partially by stating the CUST-NNNNN format. However, that format is already encoded in the schema's regex pattern, so the description mostly restates structured data without adding meaning about where the ID comes from or how to obtain it.

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 ("Returns the customer record") plus the ID format and the notable payload fields (account status, refundable balance). It is clear on its own but does nothing to distinguish it from the sibling trigger_refund, which is the obvious adjacent action for a record that exposes a refundable balance.

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 explicit when-to-use, when-not-to-use, or alternative named. The only hint is that the tool is a lookup keyed by customer ID, which leaves the agent to infer that trigger_refund is the follow-up mutation rather than reading it here.

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

trigger_refundB

Issues a refund to an active customer and reduces their refundable balance. The amount must be greater than 0, at most 1,000,000, have at most 2 decimal places, and not exceed the refundable balance. The reason must be 10 to 500 characters after trimming.

NameRequiredDescriptionDefault
amountYes
reasonYes
customer_idYes

Output Schema

NameRequiredDescription
amountYes
reasonYes
statusYes
currencyYes
refundIdYes
customerIdYes
processedAtYes
remainingRefundableBalanceYes

TDQS

B3.3/5.0
Behavior3/5

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

With no annotations, the description carries the full behavioral burden; it does disclose the key side effect (the refundable balance is reduced) and the validation gates that cause rejection. It omits idempotency/retry behavior, required permissions, what happens for an inactive or unknown customer, and whether the reduction is immediate.

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 tight sentences, front-loaded with purpose and behavior, followed by the validation rules. No filler, and the most decision-relevant constraint (cannot exceed refundable balance) is included rather than left implicit.

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?

An output schema exists, so return values need not be described, and the amount/reason boundaries are covered. Still missing for a 3-required-param money-movement tool: how to obtain or validate `customer_id`, failure modes for inactive customers, and idempotency guarantees.

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 compensate; it adds genuinely new semantics for `amount` (at most 2 decimal places, must not exceed the refundable balance) and `reason` (length measured after trimming). However, `customer_id` is only implied via 'active customer' — the CUST-##### format and lookup requirement are never explained.

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?

Opens with a specific verb + resource + effect: issuing a refund and reducing the refundable balance. This is clearly a mutation distinct from the read-only sibling get_customer_record, though the description never names that sibling to make the contrast explicit.

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 explicit when-to-use or when-not-to-use guidance, and the only alternative tool (get_customer_record) is not mentioned, even though an agent likely needs it first to verify balance and active status. The precondition 'active customer' is the only usage hint, and it is stated as a constraint rather than as a workflow step.

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. 2 tool updatesv1.0.0
    • First observedget_customer_record
    • First observedtrigger_refund

TDQS

A3.5/5.0

Scored across 2 tools

Disambiguation5/5

The two tools have clearly distinct purposes: one retrieves a customer record, while the other performs a refund action. There is no meaningful overlap or risk of selecting the wrong tool for a given task.

Naming Consistency5/5

Both names use a consistent snake_case verb_noun pattern: get_customer_record and trigger_refund. The verbs differ, but they accurately reflect distinct actions and remain predictable.

Tool Count3/5

Two tools are enough for a minimal lookup-and-refund workflow, but the surface feels thin for a refund server. There is little room for supporting operations such as refund status checks or history retrieval.

Completeness3/5

The core path of retrieving a customer and issuing a refund is covered, but lifecycle coverage is incomplete. There are notable gaps around refund history, refund status, idempotency, or canceling/querying previously issued refunds.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • F
    license
    B
    quality
    C
    maintenance
    Enables retrieval of customer records and triggering refunds via MCP tools, supporting both stdio and streamable HTTP transports.
    2
    -
  • F
    license
    A
    quality
    B
    maintenance
    Enables clients to issue refunds over stdio through a JSON-RPC tool call backed by a ledger. Malformed arguments are rejected with a real -32602 error code instead of the SDK's default isError result.
    1
    -