金融支付与市场数据MCP服务器

MCP Servers ZA 是一个面向南非金融基础设施的 MCP 服务器集合,提供 Stitch 支付操作和 JSE 市场数据查询,并特别关注 AI 代理在支付场景下的安全、幂等与审计设计。

  • by Ameen93
  • TypeScript
  • 本地部署
  • 更新于
在 GitHub 查看 Ameen93/mcp-servers-za

使用说明

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

  1. One read-only tool run end to end against live JSE credentials. Blocked on getting the credentials, not on code.
  2. get_share_price backed 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

  1. 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.
  2. A dry run that returns a preview, and an execute that is a separate, explicitly named operation.
  3. Policy limits enforced server-side, and refusals that carry a reason code rather than a sentence.
  4. 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.
  5. A mock rail with inspectable state, and contract tests against it.
  6. The eval suite, with results published here including the failures.
  7. Registry publishing. Not until the above holds.

What already happened

Date Record
22 Sep 2026 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_payment and initiate_refund execute 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_refund takes a nonce and forwards it; create_payment takes 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 in lib/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 own beneficiaryName, 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.