使用说明
MCP Servers ZA 🇿🇦
Model Context Protocol servers for South African financial rails, built on the assumption that an agent holding a payment tool will eventually be asked to do something it should refuse.
Most MCP servers are a thin wrapper over an API. That is fine for reading a calendar. It is not fine for moving money, where the interesting questions are what the tool refuses, what it does twice, and what it can prove afterwards. This repo is an attempt to answer those three questions in public, with tests.
What is here today
A TypeScript monorepo, npm workspaces, strict TS, vitest.
| Package | What it is | Tools |
|---|---|---|
@mcp-servers-za/stitch |
MCP server over stdio for Stitch payments | create_payment, check_payment_status, list_transactions, initiate_refund, create_debicheck_mandate |
@mcp-servers-za/jse |
MCP server over stdio for JSE market data | get_quote, get_historical, search_instruments, get_sens_announcements, get_share_price (mock data — see below) |
@mcp-servers-za/shared |
The parts that are easy to get wrong | HTTP client with retry, token-bucket rate limiter, response cache, typed API clients |
Tool inputs are Zod schemas. Responses distinguish a real answer from not_available, because a market-data tool that invents a price is worse than one that declines. The one tool that does return invented prices, get_share_price, is wired to a fixture source and says so: every response carries "source": "mock" and its tool description warns the caller. Use get_quote for real prices. There are unit tests for the rate limiter, cache, fetch layer, both API clients and the masking layer, plus integration tests that run the servers against recorded HTTP responses.
npm install
npm run build
npm test
npm run dev:stitch # or dev:jse
Credentials go in .env; see .env.example. Neither server needs credentials to run its tests.
What is deliberately not here yet
Being early is not the same as being vague, so the gaps are listed rather than implied.
- Live API integration. The client layer is real and tested against recorded responses. It has not yet been run against production Stitch or JSE credentials.
- The trust core. The design is settled and the code is next: policy limits enforced server-side, an idempotency store so a replayed call returns the first result rather than a second payment, an append-only hash-chained audit log, and a hard separation between a dry run that returns a preview and an execute that requires an explicit approval step.
- The eval suite. A public, runnable set of graded tasks: what the agent does when a tool result tries to talk it into a refund it was never asked for, when a limit is exceeded, when a webhook arrives twice, when the rail returns a partial failure. Passing rates published in this README, including the failures.
- Registry publishing. Not until the above holds.
Roadmap
What is left, in the order it should be done. There are no dates: this is a side project with one pair of hands on it, and a date nobody has committed to is a worse thing to publish than an honest ordering.
To close v0.1
- One read-only tool run end to end against live JSE credentials. Blocked on getting the credentials, not on code.
get_share_pricebacked by real data instead of the fixtures it returns today, at which point the"source": "mock"flag starts telling the truth in both directions.
Then, toward v0.2 — the trust core, roughly in dependency order
- Idempotency keys on
create_payment, with a store, so a replayed call returns the first result rather than making a second payment. The client already refuses to retry a mutation on its own; this is the half a caller can get wrong. - A dry run that returns a preview, and an execute that is a separate, explicitly named operation.
- Policy limits enforced server-side, and refusals that carry a reason code rather than a sentence.
- An append-only, hash-chained audit log, so a refusal is recorded next to the request that caused it — and so masking is enforced where things get written down, not only where they get returned.
- A mock rail with inspectable state, and contract tests against it.
- The eval suite, with results published here including the failures.
- Registry publishing. Not until the above holds.
What already happened
| Date | Record |
|---|---|
v0.1 missed this date. Two of its three parts landed on 28 Sep 2026: CI runs on main, and a clean clone now builds — npm run build and npm run typecheck had been failing outright. One runnable read-only tool end to end is still outstanding; it needs live credentials. |
Why South African rails
Because nobody else is building them, and because the constraints here are specific: DebiCheck mandates, the National Credit Act, POPIA, and a payments landscape where the useful integrations are Stitch, Peach, Ozow and the card acquirers rather than Stripe. Agent tooling written for US rails does not transfer cleanly.
Design notes
These are the rules the trust core is being built to. One of the five is enforced in the code; the other four are not — the trust core is the second item under "what is deliberately not here yet", and claiming the unbuilt ones as present tense would be exactly the kind of thing this repo is supposed to be the opposite of. They are written down because the design is the part worth arguing about before the code sets, and each one is paired with what actually happens today.
- Read and write tools are separate surfaces. A read tool can be given away freely; a write tool cannot. Today: reads and writes are separate tools, but that is naming, not enforcement — a client that can reach one can reach the other. There are no per-tool privileges.
- Dry run should be the default. Execute should be an explicit, separately named operation. Today:
create_paymentandinitiate_refundexecute on the first call. There is no preview. - Idempotency should be required, not optional. A caller without a key should get an error, not a payment. Today:
initiate_refundtakes anonceand forwards it;create_paymenttakes no key at all. The client no longer retries mutations on its own, so it will not replay a charge behind your back — but a caller that retries by hand still can, and will not be told the first one landed. - Every refusal should have a reason code and land in the audit log next to the request that caused it. Today: failures come back as a text message. There is no audit log and no reason codes.
- Nothing should be logged that should not be. Account numbers and personal data should be masked at the boundary. Today: enforced, on the Stitch server. Every tool response is built by one envelope function (
packages/stitch/src/lib/response.ts) whose last step is the masker inlib/mask.ts, so a tool cannot return an unmasked payload without bypassing the only way it has to return anything at all. Account, ID, passport and tax numbers come back as their last four digits (****4567); phone numbers the same; email local parts as their first character (t***@example.co.za); payer names as initials (T. M.).bankId, the operator's ownbeneficiaryName, the reference fields, amounts, timestamps and payment IDs are passed through, deliberately — masking them would cost the caller information without protecting anyone. Error text gets a blunter rule, because it has no field names to reason about: any run of nine or more digits is cut to its last four. Two limits worth knowing: the rules key off field names, so a sensitive value arriving under a name that is not in the table will still get through, and the JSE server is not wired to this layer because its market-data payloads contain no personal data.
Status
Pre-release and moving. Interfaces will change until v0.2. Issues and opinions welcome, particularly from anyone who has run payment integrations in South Africa and knows where the bodies are.
MIT licensed. Built by Ameen Solomon, Cape Town.