customer-refund-mcp-server
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@customer-refund-mcp-serverIssue a $50 refund to customer CUST-00001 because the item arrived damaged."
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
customer-refund-mcp-server
A Model Context Protocol (MCP) server that exposes two tools over the stdio transport:
get_customer_recordreturns a customer record by ID.trigger_refundissues 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 buildThe build writes the server to dist/index.js.
Running the server
node dist/index.jsThe 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.jsThe 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.jsonWindows:
%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 |
| string | Must match |
Output (structuredContent, also serialized as JSON in a text content block):
Field | Type | Description |
| string | Customer ID |
| string | Customer name |
| string | Customer email address |
| string |
|
| string | ISO 4217 currency code |
| number | Amount that can still be refunded |
| 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 |
| string | Must match |
| number | Greater than 0, at most 1,000,000, at most 2 decimal places. Strings are not accepted. |
| 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 |
| string | Random UUID for the refund |
| string | Customer ID |
| number | Refunded amount |
| string | Currency of the customer account |
| string | Trimmed reason |
| string | Always |
| string | Processing time (ISO 8601) |
| 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 |
| active | 500.00 USD |
| suspended | 200.00 USD |
| 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_idmust be exactlyCUST-followed by five digits. Lowercase prefixes, other digit counts, empty strings and surrounding whitespace are rejected.amountmust be a JSON number. Numeric strings such as"10.00"are rejected, as are0, negative numbers, values above 1,000,000 and values with more than two decimal places such as10.001.reasonis 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 |
|
Unknown tool name | JSON-RPC error |
|
Unknown method | JSON-RPC error (SDK) |
|
Unexpected internal failure | JSON-RPC error |
|
Customer not found, account not active, amount exceeds the balance | Tool result with | 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 isMCP error -32602: Unknown tool: <name>.When
argumentsis present but is not a JSON object (for examplenullor an array), the SDK rejects the request with-32602before the tool schema runs. That response has noerror.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
paramsdo not match the shape the SDK expects for that method (for example atools/callwhosenameis not a string), the SDK responds with-32603and 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.infoandconsole.debugto stderr before it loads any other module. Modules are loaded with dynamicimport()after the rebinding, so even code that runs while a module is being loaded cannot write to stdout.ESLint enforces
no-consoleand forbidsprocess.stdoutthroughout the codebase. The only exception is the console rebinding insrc/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.jsdirectly, because npm and other wrappers write their own output to stdout.
Development
Script | Description |
| Compile TypeScript to |
| Run the built server (for local use only, not in client configs) |
| Run the server from source with |
| Type-check sources and tests |
| Run ESLint with zero warnings allowed |
| Format files with Prettier |
| Check formatting |
| Build, then run unit and integration tests |
| Build, then run tests with coverage thresholds |
| Open the MCP Inspector against |
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 stdioLicense
Available Tools
2 toolsget_customer_recordB
Returns the customer record for a customer ID in the format CUST-NNNNN, including account status and refundable balance.
| Name | Required | Description | Default |
|---|---|---|---|
| customer_id | Yes |
Output Schema
| Name | Required | Description |
|---|---|---|
| name | Yes | |
| Yes | ||
| status | Yes | |
| currency | Yes | |
| createdAt | Yes | |
| customerId | Yes | |
| refundableBalance | Yes |
TDQS
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.
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.
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.
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.
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.
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.
| Name | Required | Description | Default |
|---|---|---|---|
| amount | Yes | ||
| reason | Yes | ||
| customer_id | Yes |
Output Schema
| Name | Required | Description |
|---|---|---|
| amount | Yes | |
| reason | Yes | |
| status | Yes | |
| currency | Yes | |
| refundId | Yes | |
| customerId | Yes | |
| processedAt | Yes | |
| remainingRefundableBalance | Yes |
TDQS
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.
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.
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.
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.
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.
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.
2 tool updates
v1.0.0- First observed
get_customer_record - First observed
trigger_refund
TDQS
Scored across 2 tools
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.
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.
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.
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
Related MCP Connectors
MCP server (stdio): validate JSON against JSON Schema (draft-07 / 2020-12) via the AgentForge API
Deterministic JSON Schema validation with exact violations and conservative type normalization.
MCP server for Quaderno — tax-rate calculation, invoices, contacts, products, receipts & expenses.
Hosted Stripe MCP server for Claude, ChatGPT and Cursor, with each action switched per account.
Related MCP Servers
- FlicenseNot gradedqualityCmaintenanceProvides MCP tools for retrieving customer records and processing refunds, with strict input validation, proper JSON-RPC error mapping, and enforced stdout protocol isolation.7 npm-
- FlicenseBqualityCmaintenanceEnables retrieval of customer records and triggering refunds via MCP tools, supporting both stdio and streamable HTTP transports.2-
- FlicenseNot gradedqualityCmaintenanceEnables retrieving customer records and triggering validated refunds via JSON-RPC over stdio.-
- FlicenseAqualityBmaintenanceEnables 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-